Showing preview only (809K chars total). Download the full file or copy to clipboard to get everything.
Repository: Gadzhovski/TRACE-Forensic-Toolkit
Branch: master
Commit: 9b8503a3ad34
Files: 37
Total size: 53.0 MB
Directory structure:
gitextract_yykb5mfu/
├── .gitignore
├── LICENSE
├── README.md
├── install_macos_linux_WSL.sh
├── main.py
├── modules/
│ ├── about.py
│ ├── converter.py
│ ├── exif_tab.py
│ ├── file_carving.py
│ ├── hex_tab.py
│ ├── mainwindow.py
│ ├── metadata_tab.py
│ ├── registry.py
│ ├── text_tab.py
│ ├── unified_application_manager.py
│ ├── verification.py
│ ├── veriphone_api.py
│ └── virus_total_tab.py
├── requirements.txt
├── requirements_macos_silicon.txt
├── styles/
│ ├── dark_theme.qss
│ └── light_theme.qss
└── tools/
├── Arsenal-Image-Mounter-v3.10.257/
│ ├── Arsenal Recon - End User License Agreement.txt
│ ├── ArsenalImageMounter.deps.json
│ ├── ArsenalImageMounter.log
│ ├── ArsenalImageMounter.runtimeconfig.json
│ ├── aim_cli.runtimeconfig.json
│ ├── readme.txt
│ └── readme_cli.txt
└── sleuthkit-4.12.1-win32/
├── NEWS.txt
├── README-win32.txt
├── README.txt
├── bin/
│ └── mactime.pl
├── lib/
│ ├── libtsk.lib
│ └── libtsk_jni.lib
└── licenses/
├── IBM-LICENSE
└── cpl1.0.txt
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Ignore E01, dd, and raw image files in the root directory
/*.E01
/*.dd
/*.raw
# Ignore IDE-specific folders and files
.idea/
# Ignore config files
/config.ini
# Ignore the carved_files directory
/carved_files/
# Ignore virtual environment folders
/venv/
# Ignore Python cache
__pycache__
# Ignore build artifacts
/build/
/dist/
*.spec
build_exe.py
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 Radoslav Gadzhovski
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<h1 align="center">Toolkit for Retrieval and Analysis of Cyber Evidence (TRACE)</h1>
<p align="center">
TRACE is a digital forensic tool I developed as my final year project. It provides an intuitive interface for analyzing disk images and includes a range of functionalities to assist forensic examiners in extracting and viewing the contents of various image file formats.
</p>
<p align="center">
<img src="Icons/logo_prev_ui.png" alt="TRACE Logo" width="400"/>
</p>
## Navigation 🧭
- [Preview 👀](#preview-)
- [Features 🌟](#features-)
- [Screenshots 📸](#screenshots-)
- [Supported Image Formats 💾](#supported-image-formats-)
- [Tested File Systems 🗂️](#tested-file-systems-%EF%B8%8F)
- [Cross-Platform Compatibility 🖥️💻](#cross-platform-compatibility-%EF%B8%8F)
- [Getting Started 🚀](#getting-started-)
- [Prerequisites 🛠️](#prerequisites-)
- [Configuration ⚙️](#configuration-%EF%B8%8F)
- [Running the Tool ▶️](#running-the-tool-%EF%B8%8F)
- [Built With 🧱](#built-with-)
- [Work in Progress 🛠️](#work-in-progress-)
- [Testing & Feedback 🧪](#testing--feedback-)
- [Contributing 🤝](#contributing-)
- [Socials 👨💻](#socials-)
## Preview 👀 [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)
<p>
<br/>
<img src="Icons/readme/Preview_Dark.png" alt="TRACE Preview" width="100%"/>
<br/>
</p>
<br>
## Features 🌟 [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)
✅ ***Image Mounting**: Mount forensic disk images. (Windows only) \
✅ **Tree Viewer**: Navigate through the disk image structure, including partitions and files.\
✅ **Detailed File Analysis**: View file content in different formats, such as HEX, text, and application-specific views.\
✅ **EXIF Data Extraction**: Extract and display EXIF metadata from photos.\
✅ **Registry Viewer**: View and examine Windows registry files.\
✅ **Basic File Carving**: Recover deleted files from disk images.\
✅ **Virus Total API Integration**: Check files for malware using the Virus Total API.\
✅ **E01 Image Verification**: Verify the integrity of E01 disk images.\
✅ **Convert E01 to Raw**: Convert E01 disk images to raw format.\
✅ **Message Decoding**: Decode messages from base64, binary, and other encodings.
<br>
## Screenshots 📸 [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)
### Registry Browser 🗂️
<p>
<br/>
<img src="Icons/readme/registry.png" alt="Registry Browser" width="90%"/>
<br/>
</p>
### File Carving 🔪
<p>
<br/>
<img src="Icons/readme/carving.png" alt="File Carving" width="90%"/>
<br/>
</p>
### File Search 🔍
<p>
<br/>
<img src="Icons/readme/file_search.png" alt="Image Verification" width="80%"/>
<br/>
</p>
### Image Verification ✅
<p>
<br/>
<img src="Icons/readme/trace_verify.png" alt="Image Verification" width="70%"/>
<br/>
</p>
<br>
## Supported Image Formats 💾 [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)
| Image Format | Extensions | Split | Unsplit |
|------------------------------------------------|------------------------|---------|----------|
| EnCase® Image File (EVF / Expert Witness Format)| `*.E01` `*.Ex01` | ✔️ | ✔️ |
| SMART/Expert Witness Image File | `*.s01` | ✔️ | ✔️ |
| Single Image Unix / Linux DD / Raw | `*.dd`, `*.img`, `*.raw` | ✔️ | ✔️ |
| ISO Image File | `*.iso` | | ✔️ |
| AccessData Image File | `*.ad1` | ✔️ | ✔️ |
<br>
## Tested File Systems 🗂️ [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)
| File System | Tested |
|-------------|--------|
| NTFS | ✔️ |
| FAT32 | |
| exFAT | |
| HFS+ | |
| APFS | |
| EXT2,3,4 | |
<br>
## Cross-Platform Compatibility 🍏🐧🗔 [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)
| Operating System | Screenshot |
|------------------------------------|----------------------------------------------------------------------------------------------------------------------|
| **macOS Sonoma** 🍏 | <a href="Icons/readme/macos.png"><img src="Icons/readme/macos.png" alt="macOS Screenshot" width="900"/></a> |
| **Kali Linux 2024** 🐧 | <a href="Icons/readme/kali.png"><img src="Icons/readme/kali.png" alt="Kali Linux Screenshot" width="900"/></a> |
| **\*WSL2 - Ubuntu 22.04.3 LTS** 🐧 | <a href="Icons/readme/wsl3.png"><img src="Icons/readme/wsl3.png" alt="Kali Linux Screenshot" width="900"/></a> |
| **Windows 10** 🗔 | <a href="Icons/readme/windows10.png"><img src="Icons/readme/windows10.png" alt="Windows Screenshot" width="900"/></a> |
## Getting Started 🚀 [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)
### Installation ⚙️
#### **Windows:**
1. Install Python 3.11<br>
(⚠️ Python 3.12 is not supported)<br>
[👉 Download from python.org](https://www.python.org/downloads/release/python-3110/)
2. Install Microsoft C++ Build Tools<br>
[👉 Download Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/)
During setup, ensure the following workloads are selected:
- ✅ Desktop development with C++
- ✅ C++ build tools
3. Create and activate a virtual environment
```bash
python -m venv venv
venv\Scripts\activate
```
4. Install dependencies
```bash
pip install -r requirements.txt
```
5. Run the tool
```bash
python main.py
```
#### **macOS (Apple Silicon) & Linux (Ubuntu/WSL):**
1. Make the installation script executable:
```bash
chmod +x install_macos_linux_WSL.sh
```
2. Run the installation script:
```bash
./install_macos_linux_WSL.sh
```
The script will:
- ✅ Create and activate a Python 3.11 virtual environment
- ✅ Detect your system (macOS or Linux)
- ✅ Install required system dependencies (via Homebrew or apt)
- ✅Install the appropriate Python packages:
* `requirements_macos_silicon.txt` → macOS
* `requirements.txt` → Linux
- ✅ After installation, it will automatically activate your virtual environment and notify you that it’s ready to use.
3. Run the Tool
Once the virtual environment is activated (you’ll see `(venv)` in your terminal prompt):
```bash
python main.py
```
### Configuration ⚙️
**API Keys Configuration**:The tool integrates with VirusTotal and Veriphone APIs, and you will need to provide your own API keys to use these features. To update the API keys, go to the Options menu and select API Keys submenu.
## Built With 🧱 [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)
- [pytsk3](https://pypi.org/project/pytsk3/) - Python bindings for the SleuthKit
- [libewf-python](https://github.com/libyal/libewf) - Library to access the Expert Witness Compression Format (EWF)
- [PySide6](https://pypi.org/project/PySide6/) - Used for the GUI components.
- [Arsenal Image Mounter](https://arsenalrecon.com/products/image-mounter/) - For mounting forensic disk images.
## Work in Progress 🧑🔧 [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)
- **Cross-Platform Image Mounting**: Image mounting currently works only on Windows using the Arsenal Image Mounter executable. The aim is to make this feature work across all platforms without relying on external executables.
- **File Carving**: The verification of carved files needs improvement, as it may carve data fragments that are not actual files.
- **Color Issues in Dark Mode**: The software currently has some colour display issues on Linux and macOS systems when using dark mode. Certain UI elements may not be clearly visible or may appear incorrectly.
## Contributing 🤝 [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)
I welcome contributions from the community to help improve TRACE! If you're interested in contributing, here’s how you can get involved:
1. **Report Issues**: If you find any bugs or have suggestions for improvements, please [open an issue](https://github.com/Gadzhovski/TRACE-Forensic-Toolkit/issues) on GitHub. Provide as much detail as possible to help address the issue effectively.
2. **Submit a Pull Request**: If you have a fix or feature you’d like to contribute, please [fork the repository](https://github.com/Gadzhovski/TRACE-Forensic-Toolkit/fork), make your changes, and submit a pull request. Ensure your code adheres to the coding standards and includes tests where applicable.
## Socials 👨💻 [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)
[](https://linkedin.com/in/radoslav-gadzhovski)
<br>


================================================
FILE: install_macos_linux_WSL.sh
================================================
#!/bin/bash
set -e
# === COLORS ===
GREEN="\033[1;32m"
LIGHT_GREEN="\033[38;5;82m"
CYAN="\033[1;36m"
MAGENTA="\033[1;35m"
YELLOW="\033[1;33m"
R="\033[0m"
clear
# === Animated intro ===
animate_intro() {
local frames=("⣾" "⣷" "⣯" "⣟" "⡿" "⢿" "⣻" "⣽")
echo -ne "${MAGENTA}Launching TRACE Installer "
for i in {1..20}; do
printf "\b%s" "${frames[$((i % 8))]}"
sleep 0.08
done
echo -e "${R}\n"
}
# === Pulsing TRACE logo (with aligned borders) ===
print_banner() {
local colors=("\033[38;5;48m" "\033[38;5;118m" "\033[38;5;83m" "\033[38;5;77m")
for i in {0..3}; do
clear
echo -e "${CYAN}┌────────────────────────────────────────────────────────────────────┐${R}"
echo -e "${CYAN}│ │${R}"
echo -e "${CYAN}│${R} ${colors[$i]}████████╗██████╗ █████╗ ██████╗███████╗${R} ${CYAN}│${R}"
echo -e "${CYAN}│${R} ${colors[$i]}╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝${R} ${CYAN}│${R}"
echo -e "${CYAN}│${R} ${colors[$i]}██║ ██████╔╝███████║██║ █████╗${R} ${CYAN}│${R}"
echo -e "${CYAN}│${R} ${colors[$i]}██║ ██╔══██╗██╔══██║██║ ██╔══╝${R} ${CYAN}│${R}"
echo -e "${CYAN}│${R} ${colors[$i]}██║ ██║ ██║██║ ██║╚██████╗███████╗${R} ${CYAN}│${R}"
echo -e "${CYAN}│${R} ${colors[$i]}╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝╚══════╝${R} ${CYAN}│${R}"
echo -e "${CYAN}│ │${R}"
echo -e "${CYAN}│${R} ${MAGENTA}TRACE Forensic Toolkit Installer${R} ${CYAN}│${R}"
echo -e "${CYAN}│${R} ${YELLOW}Compatible with macOS • Linux • WSL${R} ${CYAN}│${R}"
echo -e "${CYAN}└────────────────────────────────────────────────────────────────────┘${R}\n"
sleep 0.12
done
}
# === Run intro and banner ===
animate_intro
print_banner
# === Detect OS type ===
OS_TYPE=$(uname)
if [[ "$OS_TYPE" == "Darwin" ]]; then
DETECTED_OS="macOS"
elif [[ "$OS_TYPE" == "Linux" ]]; then
if grep -qi "microsoft" /proc/version; then
DETECTED_OS="WSL"
else
DETECTED_OS="Linux"
fi
else
echo -e "${RED}❌ Unsupported OS: $OS_TYPE${R}"
echo "This installer supports macOS, Linux, and WSL only."
exit 1
fi
echo -e "${CYAN}Detected operating system:${R} ${YELLOW}$DETECTED_OS${R}\n"
# === Confirm OS or override ===
read -p "Proceed with $DETECTED_OS installation? (y/n or type 'macos'/'linux'/'wsl' to override): " USER_INPUT
USER_INPUT=$(echo "$USER_INPUT" | tr '[:upper:]' '[:lower:]')
if [[ "$USER_INPUT" == "n" ]]; then
echo -e "${RED}Installation cancelled.${R}"
exit 0
elif [[ "$USER_INPUT" == "macos" ]]; then
USER_OS="macOS"
elif [[ "$USER_INPUT" == "linux" ]]; then
USER_OS="Linux"
elif [[ "$USER_INPUT" == "wsl" ]]; then
USER_OS="WSL"
else
USER_OS="$DETECTED_OS"
fi
echo -e "\n${MAGENTA}➡ Proceeding with installation for:${R} ${YELLOW}$USER_OS${R}"
echo "------------------------------------------------------------"
echo ""
# ------------------------------------------------------------------------------
# macOS INSTALLATION
# ------------------------------------------------------------------------------
if [[ "$USER_OS" == "macOS" ]]; then
echo -e "${CYAN}🍏 Installing macOS system dependencies...${R}"
if ! command -v brew &> /dev/null; then
echo -e "${YELLOW}Homebrew not found.${R}"
read -p "Would you like to install Homebrew now? (y/n): " install_brew
if [[ "$install_brew" == "y" || "$install_brew" == "Y" ]]; then
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo -e "${GREEN}✅ Homebrew installed successfully.${R}"
else
echo -e "${RED}❌ Homebrew is required. Exiting.${R}"
exit 1
fi
fi
brew update
brew install ffmpeg poppler libmagic
echo -e "\n${CYAN}📦 Creating Python virtual environment...${R}"
python3 -m venv venv
echo -e "\n${CYAN}📥 Installing Python packages...${R}"
source venv/bin/activate
pip install --upgrade pip setuptools wheel
pip install --no-cache-dir -r requirements_macos_silicon.txt
deactivate
echo -e "\n${GREEN}✅ Installation complete!${R}"
echo -e "\nTo use ${MAGENTA}TRACE${R}, activate the environment first:"
echo -e " ${YELLOW}source venv/bin/activate${R}"
echo -e "Then start the tool with:"
echo -e " ${YELLOW}python main.py${R}\n"
echo "When finished, type 'deactivate'."
exit 0
fi
# ------------------------------------------------------------------------------
# LINUX INSTALLATION
# ------------------------------------------------------------------------------
if [[ "$USER_OS" == "Linux" ]]; then
echo -e "${CYAN}🐧 Installing Linux system dependencies...${R}"
sudo apt update
sudo apt install -y python3 python3-venv python3-pip libxcb-cursor0 libva-dev libva-drm2 ewf-tools
echo -e "\n${CYAN}📦 Creating Python virtual environment...${R}"
python3 -m venv venv
echo -e "\n${CYAN}📥 Installing Python packages...${R}"
source venv/bin/activate
pip install --upgrade pip setuptools wheel
pip install --no-cache-dir -r requirements_macos_silicon.txt
deactivate
echo -e "\n${GREEN}✅ Installation complete!${R}"
echo -e "\nTo use ${MAGENTA}TRACE${R}, activate the environment first:"
echo -e " ${YELLOW}source venv/bin/activate${R}"
echo -e "Then start the tool with:"
echo -e " ${YELLOW}python main.py${R}\n"
echo "When finished, type 'deactivate'."
exit 0
fi
# ------------------------------------------------------------------------------
# WSL INSTALLATION
# ------------------------------------------------------------------------------
if [[ "$USER_OS" == "WSL" ]]; then
echo -e "${CYAN}🐧 Installing WSL (Ubuntu) dependencies...${R}"
sudo apt update
sudo apt install -y git python3-pip python3-venv libgl1-mesa-glx libxkbcommon0 libxkbcommon-x11-0 libegl1 \
libxcb-xinerama0 qt5dxcb-plugin qt5-wayland \
libqt5dbus5 libqt5widgets5 libqt5network5 libqt5gui5 libqt5core5a libqt5svg5 qtwayland5 \
nvidia-cuda-toolkit pulseaudio
echo -e "\n${CYAN}📦 Creating Python virtual environment...${R}"
python3 -m venv venv
echo -e "\n${CYAN}📥 Installing Python packages...${R}"
source venv/bin/activate
pip install --upgrade pip setuptools wheel
pip install --no-cache-dir -r requirements_macos_silicon.txt
deactivate
echo -e "\n${GREEN}✅ Installation complete!${R}"
echo -e "\nTo use ${MAGENTA}TRACE${R}, activate the environment first:"
echo -e " ${YELLOW}source venv/bin/activate${R}"
echo -e "Then start the tool with:"
echo -e " ${YELLOW}python main.py${R}\n"
echo "When finished, type 'deactivate'."
exit 0
fi
================================================
FILE: main.py
================================================
from PySide6.QtWidgets import QApplication
from modules.mainwindow import MainWindow
if __name__ == '__main__':
app = QApplication([])
window = MainWindow()
window.show()
app.exec()
================================================
FILE: modules/about.py
================================================
from PySide6.QtCore import Qt
from PySide6.QtGui import QPixmap, QFont, QPalette, QColor
from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton, QHBoxLayout
class AboutDialog(QDialog):
def __init__(self, parent=None):
super(AboutDialog, self).__init__(parent)
self.setWindowTitle("About Trace")
layout = QVBoxLayout(self)
# Load and scale the logo
logo = QLabel(self)
pixmap = QPixmap('Icons/logo.png') # Ensure 'Icons/logo.png' is the correct path
# Adjust the logo size here
scaled_pixmap = pixmap.scaled(400, 400, Qt.KeepAspectRatio, Qt.SmoothTransformation)
logo.setPixmap(scaled_pixmap)
logo.setAlignment(Qt.AlignCenter) # Center the logo
layout.addWidget(logo)
# Software information
title_label = QLabel("Trace - Toolkit for Retrieval and Analysis of Cyber Evidence")
title_label.setAlignment(Qt.AlignCenter)
title_label.setFont(QFont('Arial', 20, QFont.Bold)) # Set the font, size, and weight
title_label.setPalette(QPalette(QColor('blue'))) # Set the text color
layout.addWidget(title_label)
version_label = QLabel("Version 1.0.0")
version_label.setAlignment(Qt.AlignCenter)
layout.addWidget(version_label)
author_label = QLabel("Author: Radoslav Gadzhovski")
author_label.setAlignment(Qt.AlignCenter)
layout.addWidget(author_label)
# Add a button to close the dialog
button_layout = QHBoxLayout()
button_layout.addStretch() # Add stretchable space on the left
close_button = QPushButton("Close")
close_button.setFixedSize(100, 30) # Set the size of the button
close_button.clicked.connect(self.close)
button_layout.addWidget(close_button) # Add the button to the layout
button_layout.addStretch() # Add stretchable space on the right
# Add the QHBoxLayout to the main QVBoxLayout
layout.addLayout(button_layout)
self.setLayout(layout)
# Set the size of the dialog
self.setFixedSize(500, 700)
================================================
FILE: modules/converter.py
================================================
import os
import subprocess
import pyewf
from PySide6.QtCore import Signal
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import (
QLabel, QFileDialog, QComboBox,
QLineEdit, QHBoxLayout, QFormLayout
)
from PySide6.QtWidgets import (
QMainWindow, QPushButton, QWidget, QRadioButton,
QGroupBox, QVBoxLayout, QMessageBox, QStackedWidget
)
# Helper Function to List Drives (For Physical and Logical Drive Selection)
def list_drives():
if os.name == "nt":
# Using PowerShell command to list drives on Windows
command = ["powershell", "-NoProfile", "Get-WmiObject Win32_DiskDrive | Select-Object Model, DeviceID"]
elif os.name == "darwin":
# Using diskutil to list drives on macOS
command = ["diskutil", "list"]
else:
raise Exception("Unsupported OS")
result = subprocess.run(command, capture_output=True, text=True)
if result.returncode != 0:
raise Exception("Failed to list drives")
return result.stdout
class Main(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Convert E01 to DD/RAW")
self.setGeometry(100, 100, 400, 400)
# set logo
self.setWindowIcon(QIcon('Icons/logo.png'))
self.stacked_widget = QStackedWidget()
self.setCentralWidget(self.stacked_widget)
self.init_ui()
def init_ui(self):
self.select_source_dialog = SelectSourceDialog(self)
self.stacked_widget.addWidget(self.select_source_dialog)
self.conversion_widget = ConversionWidget(self)
self.stacked_widget.addWidget(self.conversion_widget)
self.drive_selection_widget = DriveSelectionWidget(self)
self.stacked_widget.addWidget(self.drive_selection_widget)
self.select_source_dialog.sourceSelected.connect(self.show_specific_widget)
self.drive_selection_widget.backRequested.connect(self.show_select_source)
def show_specific_widget(self, widget_name):
if widget_name == "conversion":
self.stacked_widget.setCurrentWidget(self.conversion_widget)
elif widget_name == "folder_contents":
# Handle folder contents selection
pass # Placeholder for actual implementation
elif widget_name == "physical_drive":
self.stacked_widget.setCurrentWidget(self.drive_selection_widget)
elif widget_name == "logical_drive":
# Handle logical drive selection
pass # Placeholder for actual implementation
def show_select_source(self):
self.stacked_widget.setCurrentWidget(self.select_source_dialog)
# New Widget for Drive Selection
class DriveSelectionWidget(QWidget):
backRequested = Signal()
driveSelected = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
def init_ui(self):
layout = QVBoxLayout(self)
self.drive_combo = QComboBox()
try:
for drive in list_drives().split('\n'):
if drive.strip():
self.drive_combo.addItem(drive.strip())
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to list drives: {e}")
layout.addWidget(QLabel("Select Drive:"))
layout.addWidget(self.drive_combo)
select_button = QPushButton("Select")
select_button.clicked.connect(self.on_select_clicked)
layout.addWidget(select_button)
back_button = QPushButton("Back")
back_button.clicked.connect(lambda: self.backRequested.emit())
layout.addWidget(back_button)
def on_select_clicked(self):
selected_drive = self.drive_combo.currentText()
self.driveSelected.emit(selected_drive.split()[-1]) # Assuming the device ID is the last part
class SelectSourceDialog(QWidget):
sourceSelected = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.layout = QVBoxLayout(self)
self.group_box = QGroupBox("Select the Source Type")
self.layout.addWidget(self.group_box)
self.radio_buttons_layout = QVBoxLayout()
# Existing options
self.image_file_radio = QRadioButton("Image File")
# New source options
self.physical_drive_radio = QRadioButton("Physical Drive (not implemented)")
self.logical_drive_radio = QRadioButton("Logical Drive (not implemented)")
self.contents_of_folder_radio = QRadioButton("Contents of a Folder (not implemented)")
# Add the radio buttons to the layout
self.radio_buttons_layout.addWidget(self.image_file_radio)
self.radio_buttons_layout.addWidget(self.physical_drive_radio)
self.radio_buttons_layout.addWidget(self.logical_drive_radio)
self.radio_buttons_layout.addWidget(self.contents_of_folder_radio)
self.group_box.setLayout(self.radio_buttons_layout)
self.next_button = QPushButton("Next")
self.next_button.clicked.connect(self.on_next_clicked)
self.layout.addWidget(self.next_button)
self.close_button = QPushButton("Close")
self.close_button.clicked.connect(lambda: self.window().close())
self.layout.addWidget(self.close_button)
def on_next_clicked(self):
if self.image_file_radio.isChecked():
self.sourceSelected.emit("conversion")
elif self.contents_of_folder_radio.isChecked():
self.sourceSelected.emit("folder_contents")
elif self.physical_drive_radio.isChecked():
self.sourceSelected.emit("physical_drive")
elif self.logical_drive_radio.isChecked():
self.sourceSelected.emit("logical_drive")
class ConversionWidget(QWidget):
backRequested = Signal() # Signal to request going back to the source selection
def __init__(self, parent=None):
super().__init__(parent)
self.setGeometry(100, 100, 400, 400)
self.setWindowTitle("Convert E01 to DD/RAW")
self.setWindowIcon(QIcon('Icons/logo.png'))
self.init_ui()
def init_ui(self):
layout = QVBoxLayout(self)
form_layout = QFormLayout()
self.input_line_edit = QLineEdit()
browse_button = QPushButton("Browse...")
browse_button.clicked.connect(self.browse_file)
self.format_combo_box = QComboBox()
self.format_combo_box.addItems(["DD", "RAW"])
self.output_line_edit = QLineEdit()
output_dir_button = QPushButton("Select Output Directory...")
output_dir_button.clicked.connect(self.select_output_dir)
form_layout.addRow(QLabel("Select E01 File:"), self.input_line_edit)
form_layout.addRow(browse_button)
form_layout.addRow(QLabel("Select Output Format:"), self.format_combo_box)
form_layout.addRow(QLabel("Select Output Directory:"), self.output_line_edit)
form_layout.addRow(output_dir_button)
layout.addLayout(form_layout)
# Buttons layout
buttons_layout = QHBoxLayout()
back_button = QPushButton("Back")
back_button.clicked.connect(self.on_back_clicked)
convert_button = QPushButton("Convert")
convert_button.clicked.connect(self.convert)
buttons_layout.addWidget(back_button)
buttons_layout.addWidget(convert_button)
layout.addLayout(buttons_layout)
def on_back_clicked(self):
main_window = self.parent().parent()
main_window.show_select_source()
def browse_file(self):
filename, _ = QFileDialog.getOpenFileName(self, "Select E01 File", "", "E01 Files (*.e01)")
if filename:
self.input_line_edit.setText(filename)
def select_output_dir(self):
directory = QFileDialog.getExistingDirectory(self, "Select Output Directory")
if directory:
self.output_line_edit.setText(directory)
def convert(self):
input_path = self.input_line_edit.text()
output_format = self.format_combo_box.currentText().lower() # 'dd' or 'raw'
output_dir = self.output_line_edit.text()
if not input_path or not os.path.isfile(input_path):
QMessageBox.warning(self, "Error", "The specified E01 file does not exist.")
return
if not output_dir or not os.path.isdir(output_dir):
QMessageBox.warning(self, "Error", "The specified output directory does not exist.")
return
output_filename = f"{os.path.splitext(os.path.basename(input_path))[0]}.{output_format}"
output_path = os.path.join(output_dir, output_filename)
try:
self.perform_conversion(input_path, output_path)
QMessageBox.information(self, "Success", f"File has been successfully converted to {output_path}")
except Exception as e:
QMessageBox.critical(self, "Conversion Failed", f"An error occurred: {str(e)}")
def perform_conversion(self, input_path, output_path):
normalized_input_path = os.path.normpath(input_path)
filenames = pyewf.glob(normalized_input_path)
ewf_handle = pyewf.handle()
ewf_handle.open(filenames)
with open(output_path, 'wb') as output_file:
buffer_size = ewf_handle.bytes_per_sector
while True:
data = ewf_handle.read(buffer_size)
if not data:
break
output_file.write(data)
ewf_handle.close()
================================================
FILE: modules/exif_tab.py
================================================
from io import BytesIO as io_BytesIO
from PIL import Image
from PIL.ExifTags import TAGS
from PySide6.QtWidgets import QWidget, QVBoxLayout, QTextEdit
class ExifViewerManager:
def __init__(self):
self.exif_data = None
@staticmethod
def get_exif_data_from_content(file_content):
"""Extract EXIF data from the given file content."""
try:
# Open the image from the given content
image = Image.open(io_BytesIO(file_content))
# Return None if the image format doesn't support EXIF
if image.format != "JPEG":
return None
# Return the extracted EXIF data
return image._getexif()
except Exception as e:
print(f"Error extracting EXIF data: {e}")
return None
def load_exif_data(self, file_content):
"""Load and process the EXIF data from the file content."""
exif_data = self.get_exif_data_from_content(file_content)
structured_data = []
# If EXIF data is found, process it
if exif_data:
for key in exif_data.keys():
if key in TAGS and isinstance(exif_data[key], (str, bytes)):
try:
tag_name = TAGS[key]
tag_value = exif_data[key]
structured_data.append((tag_name, tag_value))
except Exception as e:
print(f"Error processing key {key}: {e}")
return structured_data
else:
return None
class ExifViewer(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
# Initialize the manager to handle EXIF data
self.manager = ExifViewerManager()
self.init_ui()
def init_ui(self):
"""Initialize the user interface components."""
# Set up a read-only text edit for displaying the EXIF data
self.text_edit = QTextEdit(self)
self.text_edit.setStyleSheet("border: 0px;")
self.text_edit.setReadOnly(True)
self.text_edit.setContentsMargins(0, 0, 0, 0)
# Create the layout and add the text edit to it
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.text_edit)
# Set the layout for the widget
self.setLayout(layout)
def display_exif_data(self, exif_data):
"""Display the provided EXIF data in the text edit."""
if exif_data:
# Format the EXIF data as an HTML table with CSS styling
exif_table = f"""
<style>
body {{
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
}}
table {{
width: 100%;
border-collapse: collapse;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}}
td, th {{
border: 1px solid #ddd;
padding: 8px;
word-wrap: break-word;
text-align: left;
}}
th {{
background-color: #ddd; /* Changed color to a light gray */
color: black; /* Changed color to black */
}}
tr:nth-child(even) {{
background-color: #f2f2f2;
}}
tr:hover {{
background-color: #ddd;
}}
</style>
<table>
"""
for key, value in exif_data:
exif_table += f"<tr><td><b>{key}</b></td><td>{value}</td></tr>"
exif_table += "</table>"
self.text_edit.setHtml(exif_table)
else:
# Clear the text edit if there's no EXIF data to display
self.text_edit.clear()
def clear_content(self):
"""Clear the displayed content."""
self.text_edit.clear()
def load_and_display_exif_data(self, file_content):
"""Load the EXIF data from the file content and display it."""
exif_data = self.manager.load_exif_data(file_content)
self.display_exif_data(exif_data)
================================================
FILE: modules/file_carving.py
================================================
import datetime
import io
import os
import struct
import time
import zipfile
from concurrent.futures import ThreadPoolExecutor
import cv2
from PIL import Image, UnidentifiedImageError
from PIL.ExifTags import TAGS
from PyPDF2 import PdfReader
from PyPDF2.errors import PdfReadError
from PySide6.QtCore import QSize, QUrl, QRectF
from PySide6.QtCore import Qt
from PySide6.QtCore import Signal, Slot
from PySide6.QtGui import QIcon, QAction, QDesktopServices, QPixmap, QPainter, QImage
from PySide6.QtSvg import QSvgRenderer
from PySide6.QtWidgets import QListWidget, QListWidgetItem, QToolBar, QSizePolicy, QHBoxLayout, \
QCheckBox, QHeaderView
from PySide6.QtWidgets import QMenu
from PySide6.QtWidgets import QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, QPushButton, QLabel, QTabWidget
from moviepy.editor import VideoFileClip
from pdf2image import convert_from_path
class NumericTableWidgetItem(QTableWidgetItem):
def __lt__(self, other):
self_value = self.text().split()[0] # Extract numeric part of the text
other_value = other.text().split()[0] # Extract numeric part of the text
self_unit = self.text().split()[1] # Extract unit part of the text
other_unit = other.text().split()[1] # Extract unit part of the text
units = {'B': 0, 'KB': 1, 'MB': 2, 'GB': 3, 'TB': 4}
# Convert to bytes for comparison
self_bytes = float(self_value) * (1024 ** units[self_unit])
other_bytes = float(other_value) * (1024 ** units[other_unit])
return self_bytes < other_bytes
class FileCarvingWidget(QWidget):
file_carved = Signal(str, str, str, str, str) # Unified signal for file carving
def __init__(self, parent=None):
super().__init__(parent)
self.main_window = parent # Store reference to MainWindow before it gets reparented by tab widget
self.image_handler = None
self.executor = ThreadPoolExecutor(max_workers=4) # ThreadPoolExecutor for background tasks
self.carved_files = []
self.carved_file_names = set() # Track carved file names to avoid duplicates
self.allocation_map = [] # Map of allocated disk regions to skip during carving
self.init_ui()
def init_ui(self):
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0) # Set the spacing to zero
self.toolbar = QToolBar()
self.toolbar.setContentsMargins(0, 0, 0, 0)
self.layout.addWidget(self.toolbar)
self.icon_label = QLabel()
self.icon_label.setPixmap(QPixmap('Icons/icons8-carving-64.png'))
self.icon_label.setFixedSize(48, 48)
self.toolbar.addWidget(self.icon_label)
self.title_label = QLabel("File Carving")
self.title_label.setStyleSheet("""
QLabel {
font-size: 20px; /* Slightly larger size for the title */
color: #37c6d0; /* Hex color for the text */
font-weight: bold; /* Make the text bold */
margin-left: 8px; /* Space between icon and label */
}
""")
self.toolbar.addWidget(self.title_label)
self.spacer = QLabel()
self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
self.toolbar.addWidget(self.spacer)
self.table_widget = self.create_table_widget()
self.table_widget.resizeEvent = self.handle_resize_event
self.list_widget = self.create_list_widget()
self.fileTypeLayout = QHBoxLayout()
self.fileTypes = {"All": QCheckBox("All"), "PDF": QCheckBox("PDF"), "JPG": QCheckBox("JPG"),
"PNG": QCheckBox("PNG"), "GIF": QCheckBox("GIF"), "WAV": QCheckBox("WAV"),
"MOV": QCheckBox("MOV"), "WMV": QCheckBox("WMV"), "ZIP": QCheckBox("ZIP"),
'BMP': QCheckBox("BMP")}
for fileType, checkBox in self.fileTypes.items():
self.fileTypeLayout.addWidget(checkBox)
# Adding a widget to contain the file type checkboxes
self.fileTypeWidget = QWidget()
self.fileTypeWidget.setLayout(self.fileTypeLayout)
self.toolbar.addWidget(self.fileTypeWidget)
self.start_button = QPushButton("Start")
self.start_button.clicked.connect(self.start_carving)
self.toolbar.addWidget(self.start_button)
self.stop_button = QPushButton("Stop")
self.stop_button.clicked.connect(self.stop_carving)
self.stop_button.setEnabled(False)
self.toolbar.addWidget(self.stop_button)
self.layout.addWidget(self.tab_widget)
self.file_carved.connect(self.display_carved_file)
def create_table_widget(self):
table_widget = QTableWidget()
table_widget.setColumnCount(6) # Id, Name, Size, Type, Modification Date, File Path
table_widget.setSelectionBehavior(QTableWidget.SelectRows)
table_widget.setEditTriggers(QTableWidget.NoEditTriggers)
table_widget.setSortingEnabled(True)
table_widget.verticalHeader().setVisible(False)
table_widget.setObjectName("fileCarvingTable") # For CSS styling
# Set size policy to expand with window
table_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# Use alternate row colors (matching Listing tab)
table_widget.setAlternatingRowColors(True)
table_widget.setIconSize(QSize(24, 24))
# Enable horizontal scrolling for smaller windows (matching Listing tab)
table_widget.setHorizontalScrollMode(QTableWidget.ScrollPerPixel)
table_widget.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
# Configure header - all columns use Interactive mode for horizontal scrolling
header = table_widget.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.Interactive) # Id - fixed, manually resizable
header.setSectionResizeMode(1, QHeaderView.Interactive) # Name - fixed, manually resizable
header.setSectionResizeMode(2, QHeaderView.Interactive) # Size - fixed, manually resizable
header.setSectionResizeMode(3, QHeaderView.Interactive) # Type - fixed, manually resizable
header.setSectionResizeMode(4, QHeaderView.Interactive) # Modification Date - fixed, manually resizable
header.setSectionResizeMode(5, QHeaderView.Interactive) # File Path - fixed, manually resizable
# Set column widths (matching Listing tab style)
table_widget.setColumnWidth(0, 100) # Id - compact
table_widget.setColumnWidth(1, 400) # Name - widest (matching Listing tab)
table_widget.setColumnWidth(2, 100) # Size - compact (matching Listing tab)
table_widget.setColumnWidth(3, 100) # Type - compact (matching Listing tab)
table_widget.setColumnWidth(4, 160) # Modification Date - matching timestamp columns in Listing
table_widget.setColumnWidth(5, 1100) # File Path - wide (matching Listing tab)
# Set header alignment (matching Listing tab)
header.setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter)
# Set the header labels
table_widget.setHorizontalHeaderLabels(['Id', 'Name', 'Size', 'Type', 'Modification Date', 'File Path'])
# Context menu and click handlers
table_widget.setContextMenuPolicy(Qt.CustomContextMenu)
table_widget.customContextMenuRequested.connect(self.open_context_menu)
table_widget.cellClicked.connect(self.on_carved_file_clicked)
self.tab_widget = QTabWidget()
self.tab_widget.addTab(table_widget, "File List")
return table_widget
def create_list_widget(self):
list_widget = QListWidget()
list_widget.setViewMode(QListWidget.IconMode)
list_widget.setIconSize(QSize(120, 120))
list_widget.setResizeMode(QListWidget.Adjust)
list_widget.setUniformItemSizes(True)
list_widget.setSpacing(5)
list_widget.setContextMenuPolicy(Qt.CustomContextMenu)
list_widget.customContextMenuRequested.connect(self.open_context_menu)
# Connect click event to open file in internal viewer
list_widget.itemClicked.connect(self.on_carved_file_clicked)
toolbar = QToolBar()
# Define actions
action_small_size = (QAction("Small Size", self))
action_small_size.setIcon(QIcon('Icons/icons8-small-icons-50.png'))
action_medium_size = (QAction("Medium Size", self))
action_medium_size.setIcon(QIcon('Icons/icons8-medium-icons-50.png'))
action_large_size = (QAction("Large Size", self))
action_large_size.setIcon(QIcon('Icons/icons8-large-icons-50.png'))
# Set icons
# Connect actions to new slot methods
action_small_size.triggered.connect(self.set_small_size)
action_medium_size.triggered.connect(self.set_medium_size)
action_large_size.triggered.connect(self.set_large_size)
# Add actions to the toolbar
toolbar.addAction(action_small_size)
toolbar.addAction(action_medium_size)
toolbar.addAction(action_large_size)
# Create a layout and add the toolbar and the list widget to it
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(toolbar)
layout.addWidget(list_widget)
# Create a new widget, set its layout and add it to the tab widget
widget = QWidget()
widget.setLayout(layout)
self.tab_widget.addTab(widget, "Thumbnails")
return list_widget
@staticmethod
def center_crop_to_square(pixmap, target_size):
"""Crop pixmap to center square and scale to target size for uniform thumbnails."""
if pixmap.isNull():
return pixmap
width = pixmap.width()
height = pixmap.height()
if width == height:
# Already square, just scale
return pixmap.scaled(target_size, target_size, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
# Determine the crop size (smaller dimension)
crop_size = min(width, height)
# Calculate crop position to center the crop
x = (width - crop_size) // 2
y = (height - crop_size) // 2
# Crop to square
cropped = pixmap.copy(x, y, crop_size, crop_size)
# Scale to target size
return cropped.scaled(target_size, target_size, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
@staticmethod
def render_svg_to_pixmap(svg_path, target_size):
"""Render SVG file at target resolution for crisp icons."""
renderer = QSvgRenderer(svg_path)
if not renderer.isValid():
return QPixmap()
# Create QImage at target size with transparency
image = QImage(target_size, target_size, QImage.Format_ARGB32)
image.fill(Qt.transparent)
# Render SVG onto the image
painter = QPainter(image)
renderer.render(painter, QRectF(0, 0, target_size, target_size))
painter.end()
# Convert QImage to QPixmap
return QPixmap.fromImage(image)
def set_icon_size(self, size):
self.list_widget.setIconSize(QSize(size, size))
for index in range(self.list_widget.count()):
item = self.list_widget.item(index)
item.setSizeHint(QSize(size + 10, size + 25)) # Compact padding with space for text
def set_small_size(self):
self.set_icon_size(80)
def set_medium_size(self):
self.set_icon_size(120)
def set_large_size(self):
self.set_icon_size(180)
def start_carving(self):
self.start_button.setEnabled(False)
self.stop_button.setEnabled(True)
self.clear_ui()
self.carved_files.clear()
self.carved_file_names.clear()
# Ensure the 'carved_files' and 'thumbnails' directories exist
if not os.path.exists("carved_files"):
os.makedirs("carved_files")
thumbnail_folder = os.path.join("carved_files", "thumbnails")
if not os.path.exists(thumbnail_folder):
os.makedirs(thumbnail_folder)
# Build allocation map for all partitions to skip allocated files
print("Building allocation map for allocated files...")
self.allocation_map = []
try:
partitions = self.image_handler.get_partitions()
if partitions:
# Process each partition
for partition_info in partitions:
# partition_info is (addr, desc, start, len)
start_offset = partition_info[2] # start offset in sectors
# Build allocation map for this partition
partition_map = self.image_handler.build_allocation_map(start_offset)
self.allocation_map.extend(partition_map)
print(f" Partition at offset {start_offset}: {len(partition_map)} allocated regions")
else:
# No partitions, try offset 0 (single filesystem)
if self.image_handler.has_filesystem(0):
partition_map = self.image_handler.build_allocation_map(0)
self.allocation_map.extend(partition_map)
print(f" Single filesystem: {len(partition_map)} allocated regions")
# Sort the combined allocation map
self.allocation_map.sort(key=lambda x: x[0])
print(f"Total allocated regions to skip: {len(self.allocation_map)}")
except Exception as e:
print(f"Warning: Could not build allocation map: {e}")
print("Will carve from entire disk (may include duplicates)")
self.allocation_map = []
selected_file_types = [fileType.lower() for fileType, checkbox in self.fileTypes.items() if
checkbox.isChecked()]
self.executor.submit(self.carve_files, selected_file_types)
def stop_carving(self):
self.executor.shutdown(wait=True) # Properly shutdown the executor
self.start_button.setEnabled(True) # Re-enable the start button
self.stop_button.setEnabled(False) # Disable the stop button
def set_image_handler(self, image_handler):
self.image_handler = image_handler
self.start_button.setEnabled(True)
@staticmethod
def is_offset_allocated(offset, chunk_size, allocation_map):
"""
Check if a given offset range overlaps with any allocated regions."""
if not allocation_map:
return False
chunk_end = offset + chunk_size
# Binary search to find potential overlapping regions
# We need to check if our chunk [offset, chunk_end) overlaps with any allocated region
left, right = 0, len(allocation_map)
while left < right:
mid = (left + right) // 2
alloc_start, alloc_end = allocation_map[mid]
# Check for overlap: two ranges overlap if one starts before the other ends
if offset < alloc_end and chunk_end > alloc_start:
return True
# If our chunk is entirely before this allocated region, search left half
if chunk_end <= alloc_start:
right = mid
# If our chunk is entirely after this allocated region, search right half
else:
left = mid + 1
return False
def open_context_menu(self, position):
menu = QMenu()
open_location_action = QAction("Open File Location")
open_location_action.triggered.connect(self.open_file_location)
open_image_action = QAction("Open Externally")
open_image_action.triggered.connect(self.open_image)
menu.addAction(open_location_action)
menu.addAction(open_image_action)
menu.exec_(self.table_widget.viewport().mapToGlobal(position))
def open_image(self):
if self.tab_widget.currentIndex() == 0: # If the table tab is active
current_item = self.table_widget.currentItem()
else: # If the thumbnail tab is active
current_item = self.list_widget.currentItem()
if current_item:
file_name = current_item.text()
for file_info in self.carved_files:
if file_info[0] == file_name:
file_path = file_info[3] # The file path is now at index 3
QDesktopServices.openUrl(QUrl.fromLocalFile(file_path))
break
def open_file_location(self):
current_item = self.list_widget.currentItem()
if current_item:
file_name = current_item.text()
for file_info in self.carved_files:
if file_info[0] == file_name:
file_path = file_info[3] # The file path is now at index 3
QDesktopServices.openUrl(QUrl.fromLocalFile(os.path.dirname(file_path)))
break
def get_carved_timestamp(self, file_name):
"""Get the preserved timestamp for a carved file."""
for file_info in self.carved_files:
if file_info[0] == file_name:
return file_info[4] # Modification date at index 4
return None
def on_carved_file_clicked(self, *args):
"""Handle click on carved file to display in internal viewer.
Reads file content directly from disk image (forensically sound) instead of
from the carved file on disk. This ensures we're analyzing the original data.
"""
# Get clicked file name (works for both table cellClicked and list itemClicked)
if len(args) == 2: # cellClicked(row, column) from table
row = args[0]
# Get file name from column 1 (Name column, column 0 is Id)
file_name_item = self.table_widget.item(row, 1)
if not file_name_item:
return
file_name = file_name_item.text()
elif len(args) == 1: # itemClicked(item) from list
item = args[0]
file_name = item.text()
else:
return
# Find file info in carved_files list
for file_info in self.carved_files:
if file_info[0] == file_name:
file_size_str = file_info[1] # Size at index 1
file_type = file_info[2] # Type at index 2
file_size = int(file_size_str)
try:
# Extract disk offset from filename (hex format without extension)
offset_hex = os.path.splitext(file_name)[0]
offset = int(offset_hex, 16)
# Read file content directly from disk image (forensically sound!)
if not self.image_handler:
print("No image handler available")
return
file_content = self.image_handler.read(offset, file_size)
if not file_content:
print(f"Unable to read content from offset {hex(offset)}")
return
# Create data dict for viewer (matches mainwindow's format)
data = {
'name': file_name,
'size': file_size,
'type': file_type,
'offset': offset,
'is_carved': True, # Flag indicating this is a carved file
'source': 'carved_file',
'file_content': file_content, # Include content so metadata viewer doesn't re-read
'carved_timestamp': self.get_carved_timestamp(file_name) # Get original timestamp if available
}
# Get MainWindow and call update_viewer_with_file_content
if self.main_window and hasattr(self.main_window, 'update_viewer_with_file_content'):
self.main_window.update_viewer_with_file_content(file_content, data)
else:
print("MainWindow not found or missing update_viewer_with_file_content method")
except Exception as e:
print(f"Error opening carved file in viewer: {e}")
import traceback
traceback.print_exc()
break
def setup_buttons(self):
self.start_button.setEnabled(False)
self.start_button.clicked.connect(self.start_carving_thread)
self.stop_button.setEnabled(False)
self.stop_button.clicked.connect(self.stop_carving_thread)
def start_carving_thread(self):
self.start_button.setEnabled(False)
self.stop_button.setEnabled(True)
# Launch carving in a background thread
self.executor.submit(self.carve_files)
def stop_carving_thread(self):
self.executor.shutdown(wait=False)
self.start_button.setEnabled(True)
self.stop_button.setEnabled(False)
def is_valid_file(self, data, file_type):
try:
if file_type == 'pdf':
# Validate PDF by trying to read it with PyPDF2
PdfReader(io.BytesIO(data))
elif file_type in ['jpg', 'jpeg', 'png', 'gif']:
# Validate images by attempting to open them with PIL
image = Image.open(io.BytesIO(data))
image.verify() # This will not load the image but only parse it
elif file_type == 'bmp':
return True
elif file_type == 'wav':
# Basic WAV validation could check for the RIFF header, file size, etc.
if not data.startswith(b'RIFF') or not b'WAVE' in data[:12]:
return False
# Additional WAV format checks could be implemented here
elif file_type == 'mov':
return True # For now, we'll assume all MOV files are valid
else:
return True
return True
except (IOError, UnidentifiedImageError, PdfReadError, ValueError) as e:
print(f"Error validating file of type {file_type}: {str(e)}")
return False
def carve_pdf_files(self, chunk, global_offset):
pdf_start_signature = b'%PDF-'
pdf_linearization_signature = b'/Linearized'
pdf_end_signature = b'%%EOF'
offset = 0
while offset < len(chunk):
start_index = chunk.find(pdf_start_signature, offset)
if start_index == -1:
break
linearization_index = chunk.find(pdf_linearization_signature, start_index, start_index + 1024)
if linearization_index != -1:
file_size_start = chunk.find(b'/L ', linearization_index, linearization_index + 1024) + 3
file_size_end = chunk.find(b'/', file_size_start)
if file_size_end == -1:
file_size_end = chunk.find(b' ', file_size_start)
if file_size_end != -1:
try:
file_size = int(chunk[file_size_start:file_size_end].split()[0])
pdf_content = chunk[start_index:start_index + file_size]
if self.is_valid_file(pdf_content, 'pdf'):
self.save_file(pdf_content, 'pdf', global_offset + start_index, file_size)
offset = start_index + file_size
continue
except ValueError:
pass
end_index = chunk.find(pdf_end_signature, start_index)
if end_index != -1:
end_index += len(pdf_end_signature)
pdf_content = chunk[start_index:end_index]
if self.is_valid_file(pdf_content, 'pdf'):
self.save_file(pdf_content, 'pdf', global_offset + start_index, end_index - start_index)
offset = end_index
else:
offset = start_index + 1
def carve_wav_files(self, chunk, offset):
wav_start_signature = b'RIFF'
offset = 0
while offset < len(chunk):
start_index = chunk.find(wav_start_signature, offset)
if start_index == -1:
break
if chunk[start_index + 8:start_index + 12] != b'WAVE':
offset = start_index + 4
continue
file_size_bytes = chunk[start_index + 4:start_index + 8]
file_size = int.from_bytes(file_size_bytes, byteorder='little') + 8
if start_index + file_size > len(chunk):
wav_content = chunk[start_index:]
offset = len(chunk)
else:
wav_content = chunk[start_index:start_index + file_size]
offset = start_index + file_size
if self.is_valid_file(wav_content, 'wav'):
self.save_file(wav_content, 'wav', 'carved_files', start_index)
def carve_mov_files(self, chunk, offset):
mov_signatures = [
# b'ftyp', b'moov', b'mdat', #b'pnot', b'udta', #b'uuid',
# b'moof', b'free', b'skip', b'jP2 ', b'wide', b'load',
# b'ctab', b'imap', b'matt', b'kmat', b'clip', b'crgn',
# b'sync', b'chap', b'tmcd', b'scpt', b'ssrc', b'PICT'
b'moov', b'mdat', b'free', b'wide'
]
mov_file_found = False
mov_data = b''
mov_file_offset = offset
mov_file_size = 0
while offset < len(chunk):
if offset + 8 > len(chunk):
# Not enough data for an atom header
break
atom_size = int.from_bytes(chunk[offset:offset + 4], 'big')
atom_type = chunk[offset + 4:offset + 8]
if atom_type not in mov_signatures:
if mov_file_found:
# End of MOV file
break
else:
# Not a MOV file or just a stray header, skip ahead
offset += 4
continue
mov_file_found = True
mov_file_size += atom_size
if offset + atom_size > len(chunk):
# Atom extends beyond this chunk, store what we have and wait for more data
mov_data += chunk[offset:]
break
else:
# We have the whole atom, store it
mov_data += chunk[offset:offset + atom_size]
offset += atom_size
if mov_file_found and mov_data:
# file_name = f"carved_{mov_file_offset}.mov"
# file_path = os.path.join("carved_files", file_name)
# self.save_file(mov_data, 'mov', file_path)
self.save_file(mov_data, 'mov', 'carved_files', mov_file_offset)
def carve_jpg_files(self, chunk, offset):
jpg_start_signature = b'\xFF\xD8\xFF'
jpg_end_signature = b'\xFF\xD9'
offset = 0
while offset < len(chunk):
start_index = chunk.find(jpg_start_signature, offset)
if start_index == -1:
break
end_index = chunk.find(jpg_end_signature, start_index)
if end_index != -1:
jpg_content = chunk[start_index:end_index + len(jpg_end_signature)]
# Check if it's a valid JPG file
if self.is_valid_file(jpg_content, 'jpg'):
self.save_file(jpg_content, 'jpg', 'carved_files', start_index)
offset = end_index + len(jpg_end_signature)
else:
offset = start_index + 1 # Continue searching
def carve_gif_files(self, chunk, offset):
gif_start_signature = b'\x47\x49\x46\x38'
gif_end_signature = b'\x00\x3B'
offset = 0
while offset < len(chunk):
start_index = chunk.find(gif_start_signature, offset)
if start_index == -1:
break
end_index = chunk.find(gif_end_signature, start_index)
if end_index != -1:
gif_content = chunk[start_index:end_index + len(gif_end_signature)]
# Check if it's a valid GIF file
if self.is_valid_file(gif_content, 'gif'):
self.save_file(gif_content, 'gif', 'carved_files', start_index)
offset = end_index + len(gif_end_signature)
else:
offset = start_index + 1
def carve_png_files(self, chunk, offset):
png_start_signature = b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'
png_end_signature = b'\x49\x45\x4E\x44\xAE\x42\x60\x82'
offset = 0
while offset < len(chunk):
start_index = chunk.find(png_start_signature, offset)
if start_index == -1:
break
end_index = chunk.find(png_end_signature, start_index)
if end_index != -1:
png_content = chunk[start_index:end_index + len(png_end_signature)]
# Check if it's a valid PNG file
if self.is_valid_file(png_content, 'png'):
self.save_file(png_content, 'png', 'carved_files', start_index)
offset = end_index + len(png_end_signature)
else:
offset = start_index + 1
def carve_wmv_files(self, chunk, offset):
# Define ASF header signature
asf_header_signature = b'\x30\x26\xB2\x75\x8E\x66\xCF\x11\xA6\xD9\x00\xAA\x00\x62\xCE\x6C'
current_offset = 0
while current_offset < len(chunk):
# Search for ASF header
start_index = chunk.find(asf_header_signature, current_offset)
if start_index == -1:
break
# Find the file properties object header within the first 512 bytes of the file
max_search_size = min(start_index + 512, len(chunk))
file_properties_header = b'\xA1\xDC\xAB\x8C\x47\xA9\xCF\x11\x8E\xE4\x00\xC0\x0C\x20\x53\x65'
file_properties_index = chunk.find(file_properties_header, start_index, max_search_size)
if file_properties_index == -1:
current_offset = start_index + 1
continue
# Extract the file size located at offset 40 within the object
file_size_offset = file_properties_index + 40
file_size_bytes = chunk[file_size_offset:file_size_offset + 8]
file_size = int.from_bytes(file_size_bytes, byteorder='little')
# Calculate end index based on file size
end_index = start_index + file_size
# Extract WMV content
wmv_content = chunk[start_index:end_index]
# Save the WMV content directly into the carved_files directory
self.save_file(wmv_content, 'wmv', 'carved_files', start_index + offset)
current_offset = end_index
def carve_zip_files(self, chunk, global_offset):
# Define ZIP header signatures
local_file_header_signature = b'\x50\x4b\x03\x04'
end_of_central_dir_signature = b'\x50\x4b\x05\x06'
current_pos = 0
zip_file_parts = [] # List to hold all parts of the ZIP file
while current_pos < len(chunk):
# Search for local file header
local_header_index = chunk.find(local_file_header_signature, current_pos)
if local_header_index == -1:
break
# Extract compressed size from local file header
compressed_size = struct.unpack("<I", chunk[local_header_index + 18:local_header_index + 22])[0]
# Calculate next local file header index
next_local_header_index = local_header_index + 30 + compressed_size
# Extract file content
file_content = chunk[local_header_index:next_local_header_index]
zip_file_parts.append(file_content) # Add the file content to the ZIP parts list
# Move to next local file header
current_pos = next_local_header_index
# Now, find and append the Central Directory and End of Central Directory Record
end_central_dir_index = chunk.find(end_of_central_dir_signature, current_pos)
if end_central_dir_index != -1:
# Extract comment length and calculate the total end of the ZIP file structure
comment_length = struct.unpack("<H", chunk[end_central_dir_index + 20:end_central_dir_index + 22])[0]
zip_end = end_central_dir_index + 22 + comment_length
# Extract the Central Directory and End of Central Directory Record
zip_file_structure = chunk[current_pos:zip_end]
zip_file_parts.append(zip_file_structure) # Add this to the ZIP parts list
# Combine all parts into a single ZIP file content
if zip_file_parts:
complete_zip_file_content = b''.join(zip_file_parts)
self.save_file(complete_zip_file_content, 'zip', 'carved_files', global_offset)
return None
def carve_bmp_files(self, chunk, offset):
bmp_start_signature = b'BM' # BMP files start with 'BM'
header_size = 14 # The static header size for BMP files
current_offset = 0
while current_offset < len(chunk) - header_size:
# Look for the BMP signature
start_index = chunk.find(bmp_start_signature, current_offset)
if start_index == -1:
break # No more BMP files found
# Verify there's enough chunk left to read the BMP size
if start_index + header_size > len(chunk) - 4:
break # Not enough data for size
# Read file size directly from header
bmp_file_size = int.from_bytes(chunk[start_index + 2:start_index + 6], byteorder='little')
# Sanity check for BMP size (adjust max and min size as per your need)
if bmp_file_size < 100 or bmp_file_size > 5000000:
current_offset = start_index + 2
continue # Not a valid BMP size, skip to next possible start
# Read and check dimensions for further validation
bmp_width = int.from_bytes(chunk[start_index + 18:start_index + 22], byteorder='little')
bmp_height = int.from_bytes(chunk[start_index + 22:start_index + 26], byteorder='little')
# Reasonable dimensions check (adjust max width/height as per your need)
if bmp_width <= 0 or bmp_width > 10000 or bmp_height <= 0 or bmp_height > 10000:
current_offset = start_index + 2
continue # Unreasonable dimensions, likely not a BMP
# Extract the BMP file if it's entirely within the chunk
if start_index + bmp_file_size <= len(chunk):
bmp_content = chunk[start_index:start_index + bmp_file_size]
self.save_file(bmp_content, 'bmp', 'carved_files', start_index + offset)
current_offset = start_index + bmp_file_size # Move past this BMP file
else:
break # The BMP file exceeds the chunk boundary, stop processing
# Return if more data is needed or if processing is complete
return None
def carve_files(self, selected_file_types):
try:
self.stop_carving = False
chunk_size = 1024 * 1024 * 100
offset = 0
chunks_processed = 0
chunks_skipped = 0
while offset < self.image_handler.get_size():
# Check if this chunk overlaps with allocated space
if self.is_offset_allocated(offset, chunk_size, self.allocation_map):
# Skip this chunk - it's in allocated space (existing files)
chunks_skipped += 1
offset += chunk_size
continue
chunks_processed += 1
chunk = self.image_handler.read(offset, chunk_size)
if not chunk:
break
if self.stop_carving:
self.stop_carving = False
self.start_button.setEnabled(True)
self.stop_button.setEnabled(False)
print(f"Carving stopped. Processed {chunks_processed} unallocated chunks, skipped {chunks_skipped} allocated chunks")
return
# Call the carve function for each selected file type
for file_type in selected_file_types:
if file_type == 'all':
self.carve_wav_files(chunk, offset)
self.carve_mov_files(chunk, offset)
self.carve_pdf_files(chunk, offset)
self.carve_jpg_files(chunk, offset)
self.carve_gif_files(chunk, offset)
self.carve_png_files(chunk, offset)
self.carve_wmv_files(chunk, offset)
self.carve_zip_files(chunk, offset)
self.carve_bmp_files(chunk, offset)
elif file_type == 'wav':
self.carve_wav_files(chunk, offset)
elif file_type == 'mov':
self.carve_mov_files(chunk, offset)
elif file_type == 'pdf':
self.carve_pdf_files(chunk, offset)
elif file_type == 'jpg':
self.carve_jpg_files(chunk, offset)
elif file_type == 'gif':
self.carve_gif_files(chunk, offset)
elif file_type == 'png':
self.carve_png_files(chunk, offset)
elif file_type == 'wmv':
self.carve_wmv_files(chunk, offset)
elif file_type == 'zip':
self.carve_zip_files(chunk, offset)
elif file_type == 'bmp':
self.carve_bmp_files(chunk, offset)
offset += chunk_size
print(f"Carving complete. Processed {chunks_processed} unallocated chunks, skipped {chunks_skipped} allocated chunks")
finally:
self.start_button.setEnabled(True)
self.stop_button.setEnabled(False)
@staticmethod
def extract_original_timestamp(file_content, file_type):
"""Extract original file timestamp from file headers/metadata.
Returns:
datetime object if timestamp found, None otherwise
"""
try:
if file_type.lower() in ['jpg', 'jpeg', 'png']:
# Extract EXIF DateTimeOriginal from images
try:
img = Image.open(io.BytesIO(file_content))
exif_data = img._getexif()
if exif_data:
# Look for DateTimeOriginal (tag 36867) or DateTime (tag 306)
for tag_id, value in exif_data.items():
tag_name = TAGS.get(tag_id, tag_id)
if tag_name in ['DateTimeOriginal', 'DateTime']:
# Parse format: "2024:01:15 14:30:00"
return datetime.datetime.strptime(str(value), '%Y:%m:%d %H:%M:%S')
except Exception:
pass
elif file_type.lower() == 'pdf':
# Extract CreationDate from PDF metadata
try:
pdf = PdfReader(io.BytesIO(file_content))
if pdf.metadata and '/CreationDate' in pdf.metadata:
date_str = pdf.metadata['/CreationDate']
# PDF date format: "D:20240115143000"
if date_str.startswith('D:'):
date_str = date_str[2:16] # Extract YYYYMMDDHHmmss
return datetime.datetime.strptime(date_str, '%Y%m%d%H%M%S')
except Exception:
pass
elif file_type.lower() == 'zip':
# Extract timestamp from ZIP central directory
try:
with zipfile.ZipFile(io.BytesIO(file_content)) as zf:
if zf.namelist():
# Get timestamp of first file in archive
first_file_info = zf.getinfo(zf.namelist()[0])
return datetime.datetime(*first_file_info.date_time)
except Exception:
pass
except Exception as e:
print(f"Error extracting timestamp for {file_type}: {e}")
return None
def save_file(self, file_content, file_type, file_path, offset):
# Ensure the 'carved_files' directory exists
if not os.path.exists("carved_files"):
os.makedirs("carved_files")
offset_hex = format(offset, 'x')
file_name = f"{offset_hex}.{file_type}"
file_path = os.path.join("carved_files", file_name)
# Write file content to disk
with open(file_path, "wb") as f:
f.write(file_content)
# Try to extract original timestamp from file metadata
original_timestamp = self.extract_original_timestamp(file_content, file_type)
if original_timestamp:
# Convert datetime to timestamp (seconds since epoch)
timestamp = time.mktime(original_timestamp.timetuple())
# Set both access time and modification time to preserve original timestamp
os.utime(file_path, (timestamp, timestamp))
modification_date = original_timestamp.strftime("%Y-%m-%d %H:%M:%S")
else:
# Fall back to carving time if no original timestamp found
modification_date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
file_size = str(len(file_content))
self.carved_files.append((file_name, file_size, file_type, file_path, modification_date))
self.file_carved.emit(file_name, file_size, file_type, modification_date, file_path)
self.carved_file_names.add(file_name)
@Slot(str, str, str, str, str)
def display_carved_file(self, name, size, type_, modification_date, file_path):
row = self.table_widget.rowCount()
readable_size = self.image_handler.get_readable_size(int(size))
self.table_widget.insertRow(row)
# Get file icon based on type/extension
extension = type_.lower() if type_ else 'unknown'
icon_path = self.main_window.db_manager.get_icon_path('file', extension)
# Set Id column
self.table_widget.setItem(row, 0, QTableWidgetItem(str(row + 1)))
# Set Name column with icon
name_item = QTableWidgetItem(name)
name_item.setIcon(QIcon(icon_path))
self.table_widget.setItem(row, 1, name_item)
# Set other columns
self.table_widget.setItem(row, 2, NumericTableWidgetItem(readable_size))
self.table_widget.setItem(row, 3, QTableWidgetItem(type_))
self.table_widget.setItem(row, 4, QTableWidgetItem(modification_date))
self.table_widget.setItem(row, 5, QTableWidgetItem(file_path))
# Only proceed if the file type is one of the supported formats
if type_.lower() in ['jpg', 'jpeg', 'png', 'gif', 'mov', 'pdf', 'wmv', 'bmp', 'zip', 'wav']:
file_full_path = os.path.join("carved_files", name)
thumbnail_folder = os.path.join("carved_files", "thumbnails") # Folder to save thumbnails
if not os.path.exists(thumbnail_folder):
os.makedirs(thumbnail_folder) # Create the thumbnail folder if it doesn't exist
if type_.lower() == 'mov':
thumbnail_path = os.path.join(thumbnail_folder, name.replace('.mov', '.png'))
with VideoFileClip(file_full_path) as clip:
clip.save_frame(thumbnail_path, t=0.5) # save frame at 0.5 seconds
pixmap = QPixmap(thumbnail_path)
elif type_.lower() == 'pdf':
# Convert the first page of the PDF to a thumbnail
images = convert_from_path(file_full_path)
thumbnail_path = os.path.join(thumbnail_folder, name.replace('.pdf', '.png'))
images[0].save(thumbnail_path, 'PNG')
# Create the QPixmap from the full path
pixmap = QPixmap(thumbnail_path)
elif type_.lower() == 'wmv':
capture = cv2.VideoCapture(file_full_path)
success, image = capture.read()
capture.release() # Release the capture object explicitly
if success:
thumbnail_path = os.path.join(thumbnail_folder, name.replace('.wmv', '.png'))
cv2.imwrite(thumbnail_path, image)
pixmap = QPixmap(thumbnail_path)
else:
print("Failed to extract thumbnail from WMV file")
elif type_.lower() == 'zip':
# Render ZIP icon at target size for crisp display
pixmap = self.render_svg_to_pixmap('Icons/mimetypes/application-zip.svg', 120)
elif type_.lower() == 'wav':
# Render audio icon at target size for crisp display
pixmap = self.render_svg_to_pixmap('Icons/mimetypes/audio-x-generic.svg', 120)
else:
# For image files, use the original file path
thumbnail_path = file_full_path
pixmap = QPixmap(thumbnail_path)
# Center-crop to perfect square for modern uniform gallery look (skip for SVG icons)
if type_.lower() not in ['zip', 'wav']:
pixmap = self.center_crop_to_square(pixmap, 120)
icon = QIcon(pixmap)
# Create a QListWidgetItem, set its icon, and provide a size hint to ensure the text is visible
item = QListWidgetItem(icon, name)
# Set a compact size for the QListWidgetItem with minimal padding for text
item.setSizeHint(QSize(130, 145))
# Set the item flags to not be movable and to be selectable
item.setFlags(item.flags() & ~Qt.ItemIsDragEnabled & ~Qt.ItemIsDropEnabled)
# Add the QListWidgetItem to the list widget
self.list_widget.addItem(item)
def clear(self):
self.table_widget.setRowCount(0)
self.list_widget.clear()
self.carved_files.clear()
self.start_button.setEnabled(True)
self.stop_button.setEnabled(False)
def clear_ui(self):
self.table_widget.setRowCount(0)
self.list_widget.clear()
def handle_resize_event(self, event):
# Calculate total width of the table
total_width = self.table_widget.width()
# Fixed columns: Id, Size, Type, Modification Date
fixed_width = (self.table_widget.columnWidth(0) + # Id
self.table_widget.columnWidth(2) + # Size
self.table_widget.columnWidth(3) + # Type
self.table_widget.columnWidth(4)) # Modification Date
# Remaining space for dynamic columns
remaining_width = total_width - fixed_width
# Allocate remaining space proportionally
self.table_widget.setColumnWidth(1, remaining_width // 2) # Name column
self.table_widget.setColumnWidth(5, remaining_width // 2) # File Path column
super(QTableWidget, self.table_widget).resizeEvent(event)
================================================
FILE: modules/hex_tab.py
================================================
import os
from functools import lru_cache
from PySide6.QtCore import Qt, QObject, Signal, QThread, QSize
from PySide6.QtGui import QAction, QIcon, QFont, QResizeEvent
from PySide6.QtWidgets import (QToolBar, QLabel, QMessageBox, QWidget, QVBoxLayout,
QLineEdit, QTableWidget, QHeaderView, QTableWidgetItem, QListWidget,
QSizePolicy, QFrame, QApplication, QMenu, QAbstractItemView, QFileDialog,
QToolButton, QComboBox, QSplitter)
class SearchWorker(QObject):
search_finished = Signal(list)
def __init__(self, hex_viewer_manager, query):
super().__init__()
self.hex_viewer_manager = hex_viewer_manager
self.query = query
def run(self):
matches = self.hex_viewer_manager.search(self.query)
self.search_finished.emit(matches)
class HexViewerManager:
LINES_PER_PAGE = 1024
def __init__(self, hex_content, byte_content):
self.hex_content = hex_content
self.byte_content = byte_content
self.num_total_pages = (len(hex_content) // 32) // self.LINES_PER_PAGE
if (len(hex_content) // 32) % self.LINES_PER_PAGE:
self.num_total_pages += 1
@lru_cache(maxsize=None)
def format_hex(self, page=0):
start_index = page * self.LINES_PER_PAGE * 32
end_index = start_index + (self.LINES_PER_PAGE * 32)
lines = []
chunk_starts = range(start_index, end_index, 32)
for start in chunk_starts:
if start >= len(self.hex_content):
break
lines.append(self.format_hex_chunk(start))
return '\n'.join(lines)
def format_hex_chunk(self, start):
hex_part = []
ascii_repr = []
for j in range(start, start + 32, 2):
chunk = self.hex_content[j:j + 2]
if not chunk:
break
chunk_int = int(chunk, 16)
hex_part.append(chunk.upper())
ascii_repr.append(chr(chunk_int) if 32 <= chunk_int <= 126 else '.')
hex_line = ' '.join(hex_part)
padding = ' ' * (48 - len(hex_line))
ascii_line = ''.join(ascii_repr)
line = f'0x{start // 2:08x}: {hex_line}{padding} {ascii_line}'
return line
def total_pages(self):
return self.num_total_pages
def search(self, query):
if all(part.isalnum() or part.isspace() for part in query.split()):
try:
query_bytes = bytes.fromhex(query.replace(" ", ""))
return self.search_by_hex(query_bytes)
except ValueError:
pass # Invalid hex value
if query.startswith("0x"):
return self.search_by_address(query)
else:
return self.search_by_string(query)
def search_by_address(self, address):
"""Searches for the line that contains the given address (offset)"""
try:
address_int = int(address, 16)
line_number = address_int // 16
if 0 <= line_number < len(self.byte_content) // 16:
return [line_number]
else:
return []
except ValueError:
return []
def search_by_string(self, query):
# Implementation for searching by string
matches = []
query_bytes = query.encode('utf-8')
start = 0
while start < len(self.byte_content):
position = self.byte_content.find(query_bytes, start)
if position == -1:
break
start = position + 1 # Move the start to the next character
line_number = position // 16 # Calculate line number
matches.append(line_number)
return matches
def search_by_hex(self, hex_query):
if all(part.isalnum() for part in hex_query.split()):
try:
query_bytes = bytes.fromhex(hex_query.replace(" ", ""))
except ValueError:
return [] # Invalid hex value
else:
return [] # Non-alphanumeric characters in the query
matches = []
start = 0
while start < len(self.byte_content):
position = self.byte_content[start:].find(query_bytes)
if position == -1:
break
start += position # Adjust the start to the found position
line_number = start // 16 # Calculate line number
matches.append(line_number)
start += len(query_bytes) # Move past the current match
return matches
class HexViewer(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.hex_viewer_manager = None
self.current_page = 0
self.context_menu = QMenu(self)
self.copy_action = QAction("Copy", self)
self.copy_action.triggered.connect(self.copy_to_clipboard)
self.context_menu.addAction(self.copy_action)
# Set up a context menu event handler
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_context_menu)
self.initialize_ui()
def show_context_menu(self, pos):
# Show the context menu at the cursor position
self.context_menu.exec_(self.mapToGlobal(pos))
def copy_to_clipboard(self):
selected_text = ""
# Check if any cells in the hex_table are selected
selected_indexes = self.hex_table.selectedIndexes()
if selected_indexes:
# Sort the selected indexes by row
selected_indexes.sort(key=lambda index: index.row())
for i, index in enumerate(selected_indexes):
selected_text += index.data(Qt.DisplayRole)
if index.column() == 16: # The last column (ASCII), add a newline
selected_text += "\n"
else:
next_index = selected_indexes[i + 1] if i + 1 < len(selected_indexes) else None
# Add a space if the next cell is in the same row
if next_index and next_index.row() == index.row():
selected_text += " "
# Copy the selected text to the clipboard
if selected_text:
clipboard = QApplication.clipboard()
clipboard.setText(selected_text)
def initialize_ui(self):
self.layout = QVBoxLayout()
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
self.layout.setAlignment(Qt.AlignCenter)
self.setup_toolbar()
self.layout.addWidget(self.toolbar)
# Create a QSplitter for dynamic resizing
self.splitter = QSplitter(Qt.Horizontal, self) # Horizontal splitter for hex_table and search_results_frame
# Setup Hex Table
self.setup_hex_table()
self.hex_table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # Make hex table expandable
# Add the hex table to the splitter
self.splitter.addWidget(self.hex_table)
# Create a QVBoxLayout for the search results and its title
self.search_results_layout = QVBoxLayout()
self.search_results_layout.setContentsMargins(2, 2, 2, 2)
self.search_results_layout.setSpacing(2)
self.search_results_frame = QFrame(self) # This frame will contain the title and the search results
self.search_results_frame.setMaximumWidth(180) # Make it narrower
self.search_results_frame.setObjectName("search_results_frame") # Set object name for stylesheet targeting
self.search_results_frame.setSizePolicy(QSizePolicy.Fixed,
QSizePolicy.Expanding) # Fixed width, expandable height
self.search_results_title = QLabel("Search Results", self.search_results_frame)
self.search_results_title.setAlignment(Qt.AlignCenter)
self.search_results_title.setFixedHeight(22)
self.search_results_title.setObjectName("search_results_title") # Set object name for stylesheet targeting
self.search_results_layout.addWidget(self.search_results_title)
self.search_results_widget = QListWidget(self.search_results_frame)
self.search_results_widget.setObjectName("search_results_widget") # Set object name for stylesheet targeting
self.search_results_widget.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) # Show scroll bar when needed
self.search_results_widget.itemClicked.connect(self.search_result_clicked)
self.search_results_widget.setFont(QFont("Courier", 9)) # Smaller font
self.search_results_layout.addWidget(self.search_results_widget)
self.search_results_frame.setLayout(self.search_results_layout)
# Add the search results frame to the splitter
self.splitter.addWidget(self.search_results_frame)
# Set both widgets to expand in both directions
self.splitter.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# Add the splitter to the main layout
self.layout.addWidget(self.splitter)
# Set the main layout
self.setLayout(self.layout)
def resizeEvent(self, event: QResizeEvent):
"""Handle window resizing to update layout."""
# Adjust splitter sizes dynamically based on new window dimensions
total_width = event.size().width()
total_height = event.size().height()
# Set sizes for horizontal splitter: 75% for hex_table and 25% for search_results_frame
self.splitter.setSizes([int(total_width * 0.75), int(total_width * 0.25)])
super().resizeEvent(event)
def setup_toolbar(self):
self.toolbar = QToolBar(self)
self.toolbar.setContentsMargins(0, 0, 0, 0)
self.toolbar.setMovable(False)
self.toolbar.setIconSize(QSize(16, 16)) # Reduce icon size
self.toolbar.setFixedHeight(32) # Reduce toolbar height
self.toolbar.setStyleSheet("""
QToolBar {
spacing: 2px;
padding: 1px;
}
QToolButton {
padding: 2px;
margin: 1px;
}
""")
# disable right click
self.toolbar.setContextMenuPolicy(Qt.PreventContextMenu)
# Navigation buttons
self.first_action = QAction(QIcon("Icons/icons8-thick-arrow-pointing-up-50.png"), "First", self)
self.first_action.triggered.connect(self.load_first_page)
self.toolbar.addAction(self.first_action)
self.prev_action = QAction(QIcon("Icons/icons8-left-arrow-50.png"), "Previous", self)
self.prev_action.triggered.connect(self.previous_page)
self.toolbar.addAction(self.prev_action)
# Page entry
self.page_entry = QLineEdit(self)
self.page_entry.setMaximumWidth(40)
self.page_entry.setFixedHeight(25) # Set fixed height for input
self.page_entry.setPlaceholderText("1")
self.page_entry.returnPressed.connect(self.go_to_page_by_entry)
self.toolbar.addWidget(self.page_entry)
# Total pages label
self.total_pages_label = QLabel(" of ")
self.total_pages_label.setFixedHeight(25) # Set fixed height for label
self.toolbar.addWidget(self.total_pages_label)
self.next_action = QAction(QIcon("Icons/icons8-right-arrow-50.png"), "Next", self)
self.next_action.triggered.connect(self.next_page)
self.toolbar.addAction(self.next_action)
self.last_action = QAction(QIcon("Icons/icons8-down-50.png"), "Last", self)
self.last_action.triggered.connect(self.load_last_page)
self.toolbar.addAction(self.last_action)
# Add a small spacer
spacer = QWidget(self)
spacer.setFixedSize(20, 0)
self.toolbar.addWidget(spacer)
# Add a QLabel and a QComboBox for font size to the toolbar
font_label = QLabel("Font Size: ")
font_label.setFixedHeight(25) # Set fixed height for label
self.toolbar.addWidget(font_label)
self.font_size_combobox = QComboBox(self)
self.font_size_combobox.setFixedHeight(25) # Set fixed height for combobox
self.font_size_combobox.setFixedWidth(60) # Increase width to show full numbers
self.font_size_combobox.addItems(["8", "10", "12", "14", "16", "18", "20", "24", "28", "32", "36"])
self.font_size_combobox.currentTextChanged.connect(self.update_font_size)
self.toolbar.addWidget(self.font_size_combobox)
# Add small spacer
spacer = QWidget(self)
spacer.setFixedSize(20, 0)
self.toolbar.addWidget(spacer)
self.export_button = QToolButton(self)
self.export_button.setObjectName("exportButton") # Assign a unique object name
self.export_button.setText("Export")
self.export_button.setToolButtonStyle(Qt.ToolButtonTextOnly) # Change to text only since no icon is used
self.export_button.setFixedHeight(25) # Set fixed height
self.export_button.setFixedWidth(100) # Set fixed width to show full text
self.export_button.setPopupMode(QToolButton.MenuButtonPopup) # Set the popup mode
# Add format options to the menu
self.export_menu = QMenu(self)
self.text_format_action = QAction("Text (.txt)", self)
self.text_format_action.triggered.connect(lambda: self.export_content("txt"))
self.export_menu.addAction(self.text_format_action)
self.html_format_action = QAction("HTML (.html)", self)
self.html_format_action.triggered.connect(lambda: self.export_content("html"))
self.export_menu.addAction(self.html_format_action)
self.export_button.setMenu(self.export_menu)
self.toolbar.addWidget(self.export_button)
# Add a spacer to push the following widgets to the right
spacer = QWidget(self)
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
self.toolbar.addWidget(spacer)
# Search bar components
self.search_bar = QLineEdit(self)
self.search_bar.setMaximumWidth(180) # Reduce width to save space
self.search_bar.setFixedHeight(25) # Reduce height
self.search_bar.setContentsMargins(5, 0, 5, 0) # Reduce margins
self.search_bar.setPlaceholderText("Search...")
self.search_bar.returnPressed.connect(self.trigger_search)
self.toolbar.addWidget(self.search_bar)
def update_font_size(self):
# Get the current font size from the combobox
selected_size = int(self.font_size_combobox.currentText())
# Set the new font size to the hex_table
current_font = self.hex_table.font()
current_font.setPointSize(selected_size)
self.hex_table.setFont(current_font)
# Dynamically adjust column widths based on the font size
address_width = selected_size * 10 # Proportional width for Address column
byte_width = selected_size * 3 # Proportional width for each byte column
ascii_width = selected_size * 8 # Proportional width for ASCII column
# Set column widths dynamically
self.hex_table.setColumnWidth(0, address_width) # Address column
for i in range(1, 17): # Set uniform width for each byte column
self.hex_table.setColumnWidth(i, byte_width)
self.hex_table.setColumnWidth(17, ascii_width) # ASCII column
# Update the font size for the headers as well
header_font = self.hex_table.horizontalHeader().font()
header_font.setPointSize(selected_size)
self.hex_table.horizontalHeader().setFont(header_font)
# Apply the font to all existing data cells
for row in range(self.hex_table.rowCount()):
for col in range(self.hex_table.columnCount()):
item = self.hex_table.item(row, col)
if item:
item_font = item.font()
# Ensure we have a valid font size before setting it
if selected_size > 0:
item_font.setPointSize(selected_size)
item.setFont(item_font)
# Adjust the horizontal scrollbar policy if needed
if self.hex_table.horizontalHeader().length() > self.hex_table.viewport().width():
self.hex_table.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
else:
self.hex_table.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
def setup_hex_table(self):
self.hex_table = QTableWidget()
self.hex_table.verticalHeader().setDefaultSectionSize(20) # Smaller row height
# Set the font of the hex_table
font = QFont("Courier")
font.setPointSize(10) # Default smaller font size
font.setLetterSpacing(QFont.AbsoluteSpacing, 1) # Reduce letter spacing
self.hex_table.setFont(font)
# Configure the columns and headers
self.hex_table.setColumnCount(18) # 16 bytes + 1 address + 1 ASCII
self.hex_table.setHorizontalHeaderLabels(['Address'] + [f'{i:02X}' for i in range(16)] + ['ASCII'])
self.hex_table.verticalHeader().setVisible(False)
# Set resizing policies for the header
header = self.hex_table.horizontalHeader()
header.setStyleSheet("QHeaderView::section { padding: 2px; }") # Reduce header padding
header.setDefaultSectionSize(25) # Set a smaller default size
# Address column - Resize based on content
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
# Byte columns - Fixed width for each byte column
for i in range(1, 17): # 00 to 0F columns
header.setSectionResizeMode(i, QHeaderView.Fixed)
self.hex_table.setColumnWidth(i, 25) # Smaller byte column width
# ASCII column - Stretch to fill remaining space
header.setSectionResizeMode(17, QHeaderView.Stretch)
# Adjust for remaining space in the ASCII column
header.setStretchLastSection(True)
# Set the initial column widths
self.hex_table.setColumnWidth(0, 120) # Address column initial width
self.hex_table.setColumnWidth(17, 200) # ASCII column initial width
self.hex_table.setStyleSheet("""
QTableWidget {
gridline-color: transparent;
border: 1px solid #d3d3d3;
}
QTableWidget::item {
padding: 0px;
border: none;
}
""")
self.hex_table.setShowGrid(False)
self.hex_table.setAlternatingRowColors(True)
self.hex_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
def display_hex_content(self, file_content):
hex_content = file_content.hex()
self.search_results_widget.clear()
# self.search_results_frame.setVisible(False)
# Clear the search bar text
self.search_bar.setText("")
self.hex_viewer_manager = HexViewerManager(hex_content, file_content)
self.update_navigation_states()
self.display_current_page()
# clear the page number entry
self.page_entry.setText("")
def export_content(self, selected_format):
if not self.hex_viewer_manager:
QMessageBox.warning(self, "No Content", "No content available to export.")
return
options = QFileDialog.Options()
options |= QFileDialog.ReadOnly # Allow read-only access to the selected file
if selected_format == "txt":
file_name, _ = QFileDialog.getSaveFileName(
self, "Export Hex Content", "", "Text Files (*.txt)", options=options
)
self.export_as_text(file_name)
elif selected_format == "html":
file_name, _ = QFileDialog.getSaveFileName(
self, "Export Hex Content", "", "HTML Files (*.html)", options=options
)
self.export_as_html(file_name)
else:
QMessageBox.warning(self, "Unsupported Format", "Unsupported export format selected.")
def export_as_text(self, file_name):
with open(file_name, "w") as text_file:
# Add the header line with green color using ANSI escape codes
header_line = "Address 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F ASCII"
text_file.write(header_line + "\n")
# Add an empty line
text_file.write("\n")
# Write the formatted hex content
formatted_hex = self.hex_viewer_manager.format_hex(self.current_page)
text_file.write(formatted_hex)
def export_as_html(self, file_name):
html_content = "<html><body>\n"
html_content += "<pre>\n"
# Add a smaller and less prominent header with the original text
header_line = '<div style="font-size:14px; color:#888;">Generated by Trace</div>'
html_content += header_line + "<br><br>\n"
# Add directory and file name information
directory, filename = os.path.split(file_name)
html_content += f'<span style="color:blue;">Directory: {directory}</span><br>\n'
html_content += f'<span style="color:blue;">File Name: {filename}</span><br><br>\n'
# Add the green header line
header_line = ('<span style="color:green;">Address 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F '
'ASCII</span>')
html_content += header_line + "<br>\n"
html_content += self.hex_viewer_manager.format_hex(self.current_page).replace("\n", "<br>")
html_content += "</pre>\n"
html_content += "</body></html>"
with open(file_name, "w") as html_file:
html_file.write(html_content)
def parse_hex_line(self, line):
if ":" not in line:
return None, None, None
address, rest = line.split(":", maxsplit=1)
hex_chunk, ascii_repr = rest.split(" ", maxsplit=1)
return address.strip(), hex_chunk.strip(), ascii_repr.strip()
def clear_content(self):
self.hex_table.clear()
def load_first_page(self):
try:
self.current_page = 0
self.display_current_page()
except (AttributeError, IndexError) as e:
print(f"Error occurred: {e}")
def load_last_page(self):
try:
self.current_page = self.hex_viewer_manager.total_pages() - 1
self.display_current_page()
except (AttributeError, IndexError) as e:
print(f"Error occurred: {e}")
def next_page(self):
try:
if self.current_page < self.hex_viewer_manager.total_pages() - 1:
self.current_page += 1
self.display_current_page()
except (AttributeError, IndexError) as e:
print(f"Error occurred: {e}")
def previous_page(self):
try:
if self.current_page > 0:
self.current_page -= 1
self.display_current_page()
except (AttributeError, IndexError) as e:
print(f"Error occurred: {e}")
def search_result_clicked(self, item):
address = item.text().split(":")[1].strip()
self.navigate_to_address(address)
def display_current_page(self):
formatted_hex = self.hex_viewer_manager.format_hex(self.current_page)
# Clear the table first
self.hex_table.setRowCount(0)
self.hex_table.setHorizontalHeaderLabels(['Address'] + [f'{i:02X}' for i in range(16)] + ['ASCII'])
hex_lines = formatted_hex.split('\n')
# Ensure we set the correct row count
self.hex_table.setRowCount(len(hex_lines))
# Get the current font size with fallback to default
current_font = self.hex_table.font()
current_font_size = current_font.pointSize()
if current_font_size <= 0: # Use default if invalid
current_font_size = 10 # Default font size
for row, line in enumerate(hex_lines):
address, hex_chunk, ascii_repr = self.parse_hex_line(line)
if not address or not hex_chunk: # Skip if there's an error in parsing
continue
# Set address and center-align
address_item = QTableWidgetItem(address + ":") # Add a colon after the address
address_item.setTextAlignment(Qt.AlignCenter)
item_font = address_item.font()
item_font.setPointSize(current_font_size)
address_item.setFont(item_font)
self.hex_table.setItem(row, 0, address_item)
# Set hex values and center-align
for col, byte in enumerate(hex_chunk.split()):
byte_item = QTableWidgetItem(byte)
byte_item.setTextAlignment(Qt.AlignCenter)
byte_item.setBackground(Qt.white) # Clear any previous highlight
item_font = byte_item.font()
item_font.setPointSize(current_font_size)
byte_item.setFont(item_font)
self.hex_table.setItem(row, col + 1, byte_item)
# Set ASCII representation and center-align
ascii_item = QTableWidgetItem(ascii_repr)
ascii_item.setTextAlignment(Qt.AlignCenter)
item_font = ascii_item.font()
item_font.setPointSize(current_font_size)
ascii_item.setFont(item_font)
self.hex_table.setItem(row, 17, ascii_item)
self.update_navigation_states()
def go_to_page_by_entry(self):
try:
page_num = int(self.page_entry.text()) - 1
if 0 <= page_num < self.hex_viewer_manager.total_pages():
self.current_page = page_num
self.display_current_page()
self.update_navigation_states()
else:
QMessageBox.warning(self, "Invalid Page", "Page number out of range.")
except ValueError:
QMessageBox.warning(self, "Invalid Page", "Please enter a valid page number.")
def update_navigation_states(self):
if not self.hex_viewer_manager:
self.prev_action.setEnabled(False)
self.next_action.setEnabled(False)
return
self.prev_action.setEnabled(self.current_page > 0)
self.next_action.setEnabled(self.current_page < self.hex_viewer_manager.total_pages() - 1)
self.page_entry.setText(str(self.current_page + 1))
self.total_pages_label.setText(f"of {self.hex_viewer_manager.total_pages()}")
def update_total_pages_label(self):
total_pages = self.hex_viewer_manager.total_pages()
current_page = self.current_page + 1
self.total_pages_label.setText(f"{current_page} of {total_pages}")
def trigger_search(self):
query = self.search_bar.text()
if not query:
QMessageBox.warning(self, "Search Error", "Please enter a search query.")
return
# Check if a search is already ongoing. If so, stop it before starting a new one.
if hasattr(self, 'search_thread') and self.search_thread.isRunning():
self.search_thread.quit()
self.search_thread.wait()
# Start the search in a new thread
self.search_thread = QThread()
self.search_worker = SearchWorker(self.hex_viewer_manager, query)
self.search_worker.moveToThread(self.search_thread)
# Connect signals and slots
self.search_worker.search_finished.connect(self.handle_search_results)
self.search_thread.started.connect(self.search_worker.run)
self.search_thread.finished.connect(self.cleanup_thread_resources)
# Start the thread
self.search_thread.start()
def cleanup_thread_resources(self):
# Ensure safe cleanup by checking the existence of resources before deleting
if hasattr(self, 'search_worker'):
self.search_worker.deleteLater()
del self.search_worker
if hasattr(self, 'search_thread'):
self.search_thread.deleteLater()
del self.search_thread
def closeEvent(self, event):
if hasattr(self, 'search_thread') and self.search_thread.isRunning():
self.search_thread.quit()
self.search_thread.wait()
super().closeEvent(event)
def handle_search_results(self, matches):
self.search_results_widget.clear() # Clear previous results
if matches:
for match in matches:
address = f"0x{match * 16:08x}" # Calculate the address from line number
self.search_results_widget.addItem(f"Address: {address}")
# Show the search results frame and resize the splitter to allocate more space to results
self.search_results_frame.setVisible(True)
self.splitter.setSizes([self.width() * 0.6, self.width() * 0.4]) # Adjust sizes dynamically
else:
QMessageBox.warning(self, "Search Result", "No matches found.")
# Even if no matches are found, the search results frame will still be shown
self.splitter.setSizes([self.width() * 0.75, self.width() * 0.25])
def navigate_to_address(self, address):
try:
# Convert the address string back to an integer
address_int = int(address, 16)
# Determine the line number from the address
line = address_int // 16
# The rest of the logic remains the same
self.current_page = line // self.hex_viewer_manager.LINES_PER_PAGE
self.display_current_page()
# Navigate to the specific row on that page and highlight it
row_in_page = line % self.hex_viewer_manager.LINES_PER_PAGE
self.hex_table.selectRow(row_in_page)
for col in range(1, 17):
item = self.hex_table.item(row_in_page, col)
if item:
item.setBackground(Qt.yellow)
self.update_navigation_states()
except ValueError:
QMessageBox.warning(self, "Navigation Error", "Invalid address.")
================================================
FILE: modules/mainwindow.py
================================================
import configparser
import hashlib
import os
import datetime
import pyewf
import pytsk3
import tempfile
import gc
import time
import logging
import re
from typing import Optional, Dict, Any, List, Tuple
from Registry import Registry
from sqlite3 import connect as sqlite3_connect
import subprocess
import platform
from contextlib import contextmanager
from functools import lru_cache
from PySide6.QtCore import Qt, QSize, QThread, Signal, QTimer, QMargins
from PySide6.QtGui import QIcon, QFont, QPalette, QBrush, QAction, QActionGroup, QPixmap, QPainter, QColor
from PySide6.QtCharts import QChart, QChartView, QPieSeries, QPieSlice
from PySide6.QtWidgets import (QMainWindow, QMenuBar, QMenu, QToolBar, QDockWidget, QTreeWidget, QTabWidget,
QFileDialog, QTreeWidgetItem, QTableWidget, QMessageBox, QTableWidgetItem,
QDialog, QVBoxLayout, QHBoxLayout, QInputDialog, QDialogButtonBox, QHeaderView, QLabel, QLineEdit,
QFormLayout, QApplication, QWidget, QProgressDialog, QSizePolicy, QGroupBox,
QCheckBox, QGridLayout, QScrollArea, QPushButton, QToolButton)
from modules.about import AboutDialog
from modules.converter import Main
from modules.exif_tab import ExifViewer
from modules.file_carving import FileCarvingWidget
from modules.hex_tab import HexViewer
from modules.metadata_tab import MetadataViewer
from modules.registry import RegistryExtractor
from modules.text_tab import TextViewer
from modules.unified_application_manager import UnifiedViewer
from modules.verification import VerificationWidget
from modules.veriphone_api import VeriphoneWidget
from modules.virus_total_tab import VirusTotal
SECTOR_SIZE = 512
CHUNK_SIZE = 4 * 1024 * 1024 # 4MB chunks for processing
FILE_BUFFER_SIZE = 4096 # 4KB for file operations
# ==================== CONFIGURATION CONSTANTS ====================
# Logger setup
logger = logging.getLogger('TRACE.MainWindow')
# Window dimensions
DEFAULT_WINDOW_WIDTH = 1200
DEFAULT_WINDOW_HEIGHT = 800
DEFAULT_WINDOW_X = 100
DEFAULT_WINDOW_Y = 100
# Dock sizes
VIEWER_DOCK_MIN_HEIGHT = 222
VIEWER_DOCK_MAX_WIDTH = 1200
VIEWER_DOCK_MAX_SIZE = 16777215 # Qt maximum size value
# Column widths for listing table
COLUMN_WIDTHS = {
'name': 400, # Widest - file names can be long
'inode': 50, # Compact - numbers don't vary much
'type': 50, # Compact - short text like "File", "Dir"
'size': 100, # Compact - formatted sizes
'created': 160, # Narrower - timestamps are consistent length
'accessed': 160, # Narrower - timestamps are consistent length
'modified': 160, # Narrower - timestamps are consistent length
'changed': 160, # Narrower - timestamps are consistent length
'path': 1100 # Wide - paths can be long
}
# Progress dialog settings
PROGRESS_DIALOG_WIDTH = 300
# Timeouts (in seconds)
MOUNT_TIMEOUT = 30
INFO_TIMEOUT = 10
PROCESS_TIMEOUT = 30
THREAD_SLEEP_MS = 1000 # milliseconds
# Minimum duration for progress dialog (milliseconds)
PROGRESS_MIN_DURATION = 1500
# Icon size
TREE_ICON_SIZE = 16
TABLE_ICON_SIZE = 24
TOOLBAR_ICON_SIZE = 16
# Table settings
TABLE_COLUMN_COUNT = 9
TABLE_BATCH_SIZE = 200 # Number of rows to process before updating UI
# Input field settings
INPUT_FIELD_MIN_WIDTH = 400
API_DIALOG_WIDTH = 600
# Qt maximum size constant
QT_MAX_SIZE = 16777215
# ================================================================
# Define a utility function for safe datetime conversion
def safe_datetime(timestamp):
if timestamp is None or timestamp == 0:
return "N/A"
try:
return datetime.datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S') + " UTC"
except Exception:
return "N/A"
# Utility class for common operations
class FileSystemUtils:
@staticmethod
def get_readable_size(size_in_bytes):
"""Convert bytes to a human-readable string (e.g., KB, MB, GB, TB)."""
if size_in_bytes is None:
return "0 B"
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if size_in_bytes < 1024.0:
return f"{size_in_bytes:.2f} {unit}"
size_in_bytes /= 1024.0
return f"{size_in_bytes:.2f} PB"
@staticmethod
@contextmanager
def temp_file():
"""Context manager for temporary files, ensuring cleanup."""
temp_path = None
try:
with tempfile.NamedTemporaryFile(delete=False) as temp:
temp_path = temp.name
yield temp_path
finally:
if temp_path and os.path.exists(temp_path):
os.remove(temp_path)
# Class to handle EWF images
class EWFImgInfo(pytsk3.Img_Info):
def __init__(self, ewf_handle):
self._ewf_handle = ewf_handle
super(EWFImgInfo, self).__init__(url="", type=pytsk3.TSK_IMG_TYPE_EXTERNAL)
def close(self):
self._ewf_handle.close()
def read(self, offset, size):
self._ewf_handle.seek(offset)
return self._ewf_handle.read(size)
def get_size(self):
return self._ewf_handle.get_media_size()
# ImageHandler class with optimizations
class ImageHandler:
def __init__(self, image_path):
self.image_path = image_path
self.img_info = None
self.volume_info = None
self.fs_info_cache = {}
self.fs_info = None
self.is_wiped_image = False
self._directory_cache = {} # Cache for directory contents
self._partition_cache = None # Cache for partitions
# Load the image with progress tracking
self.load_image()
def __del__(self):
"""Cleanup resources when the object is destroyed."""
self.close_resources()
def close_resources(self):
"""Explicitly close all open resources."""
# Close filesystem objects
for fs_info in self.fs_info_cache.values():
if hasattr(fs_info, 'close'):
try:
fs_info.close()
except:
pass
# Close the image
if self.img_info:
if hasattr(self.img_info, 'close'):
try:
self.img_info.close()
except:
pass
self.img_info = None
# Clear caches
self.fs_info_cache.clear()
self._directory_cache.clear()
def get_size(self):
"""Returns the size of the disk image."""
if self.img_info:
return self.img_info.get_size()
else:
raise AttributeError("Image not loaded or unsupported format.")
def read(self, offset, size):
"""Reads data from the image starting at `offset` for `size` bytes."""
if self.img_info and hasattr(self.img_info, 'read'):
return self.img_info.read(offset, size)
else:
raise NotImplementedError("The image format does not support direct reading.")
def build_allocation_map(self, start_offset):
"""Build a map of allocated disk regions by traversing the filesystem."""
allocation_map = []
try:
fs_info = self.get_fs_info(start_offset)
if not fs_info:
logger.warning(f"Unable to get filesystem info for offset {start_offset}")
return allocation_map
# Get block size for this filesystem
block_size = fs_info.info.block_size
# Recursively walk filesystem to find all allocated files
def walk_directory(directory, path="/"):
"""Recursively walk directory and collect allocated file ranges."""
try:
for entry in directory:
# Skip current and parent directory entries
if not hasattr(entry, 'info') or not hasattr(entry.info, 'name'):
continue
name = entry.info.name.name.decode('utf-8', errors='ignore')
if name in [".", ".."]:
continue
# Check if this is an allocated file
if not hasattr(entry.info, 'meta') or entry.info.meta is None:
continue
# Only process allocated files (skip deleted files)
is_allocated = bool(int(entry.info.meta.flags) & pytsk3.TSK_FS_META_FLAG_ALLOC)
if not is_allocated:
continue
# Get file size and inode
file_size = entry.info.meta.size
# Only process files with actual data
if file_size > 0:
try:
# Open the file to access its data runs
file_obj = fs_info.open_meta(inode=entry.info.meta.addr)
# Calculate byte offsets for the file's data
# This is approximate - we use the file's logical position
# For a more accurate map, we'd need to walk data runs
# but this is a reasonable approximation for most filesystems
# Get partition offset in bytes
partition_offset_bytes = start_offset * 512
# For simplicity, we'll mark regions based on inode metadata
# A more sophisticated approach would walk TSK_FS_BLOCK structures
# but pytsk3 doesn't expose block_walk easily
# Estimate file location based on inode number and size
# This is a simplified approach - actual blocks may be fragmented
inode_addr = entry.info.meta.addr
estimated_start = partition_offset_bytes + (inode_addr * block_size)
estimated_end = estimated_start + file_size
allocation_map.append((estimated_start, estimated_end))
except Exception as e:
# Skip files we can't open
logger.debug(f"Could not process file {path}{name}: {e}")
pass
# Recursively process directories
if entry.info.meta.type == pytsk3.TSK_FS_META_TYPE_DIR:
try:
sub_directory = fs_info.open_dir(inode=entry.info.meta.addr)
walk_directory(sub_directory, f"{path}{name}/")
except Exception as e:
logger.debug(f"Could not open directory {path}{name}: {e}")
pass
except Exception as e:
logger.debug(f"Error walking directory {path}: {e}")
pass
# Start walking from root directory
try:
root_dir = fs_info.open_dir(path="/")
walk_directory(root_dir)
except Exception as e:
logger.error(f"Error accessing root directory: {e}")
# Sort allocation map by start offset for efficient searching
allocation_map.sort(key=lambda x: x[0])
logger.info(f"Built allocation map with {len(allocation_map)} allocated file regions")
except Exception as e:
logger.error(f"Error building allocation map: {e}")
return allocation_map
def get_image_type(self):
"""Determine the type of the image based on its extension."""
_, extension = os.path.splitext(self.image_path)
extension = extension.lower()
ewf = [".e01", ".s01", ".l01", ".ex01"]
raw = [".raw", ".img", ".dd", ".iso",
".ad1", ".001", ".dmg", ".sparse",
".sparseimage"]
if extension in ewf:
return "ewf"
elif extension in raw:
return "raw"
else:
raise ValueError(f"Unsupported image type: {extension}")
def calculate_hashes(self, progress_callback=None):
"""Calculate the MD5, SHA1, and SHA256 hashes for the image with progress reporting."""
hash_md5 = hashlib.md5()
hash_sha1 = hashlib.sha1()
hash_sha256 = hashlib.sha256()
size = 0
total_size = 0
stored_md5, stored_sha1 = None, None
image_type = self.get_image_type()
try:
# First get total size for progress reporting
if image_type == "ewf":
filenames = pyewf.glob(self.image_path)
ewf_handle = pyewf.handle()
try:
ewf_handle.open(filenames)
total_size = ewf_handle.get_media_size()
try:
# Attempt to retrieve the stored hash values
stored_md5 = ewf_handle.get_hash_value("MD5")
stored_sha1 = ewf_handle.get_hash_value("SHA1")
except Exception as e:
logger.warning(f"Unable to retrieve stored hash values: {e}")
# Calculate hashes in chunks
while True:
chunk = ewf_handle.read(CHUNK_SIZE)
if not chunk:
break
hash_md5.update(chunk)
hash_sha1.update(chunk)
hash_sha256.update(chunk)
size += len(chunk)
# Report progress safely
if progress_callback and total_size > 0:
try:
progress_callback(size, total_size)
except Exception as e:
logger.error(f"Progress callback error: {e}")
finally:
ewf_handle.close()
elif image_type == "raw":
try:
total_size = os.path.getsize(self.image_path)
with open(self.image_path, "rb") as f:
while True:
chunk = f.read(CHUNK_SIZE)
if not chunk:
break
hash_md5.update(chunk)
hash_sha1.update(chunk)
hash_sha256.update(chunk)
size += len(chunk)
# Report progress safely
if progress_callback and total_size > 0:
try:
progress_callback(size, total_size)
except Exception as e:
logger.error(f"Progress callback error: {e}")
except Exception as e:
logger.error(f"Error reading raw image: {e}")
# Compile the computed and stored hashes in a dictionary
hashes = {
'computed_md5': hash_md5.hexdigest(),
'computed_sha1': hash_sha1.hexdigest(),
'computed_sha256': hash_sha256.hexdigest(),
'size': size,
'path': self.image_path,
'stored_md5': stored_md5,
'stored_sha1': stored_sha1
}
return hashes
except Exception as e:
logger.error(f"Error calculating hashes: {e}")
return {
'computed_md5': 'Error',
'computed_sha1': 'Error',
'computed_sha256': 'Error',
'size': 0,
'path': self.image_path,
'stored_md5': None,
'stored_sha1': None,
'error': str(e)
}
def load_image(self):
"""Load the image and retrieve volume and filesystem information."""
image_type = self.get_image_type()
try:
if image_type == "ewf":
filenames = pyewf.glob(self.image_path)
ewf_handle = pyewf.handle()
ewf_handle.open(filenames)
self.img_info = EWFImgInfo(ewf_handle)
elif image_type == "raw":
self.img_info = pytsk3.Img_Info(self.image_path)
else:
raise ValueError(f"Unsupported image type: {image_type}")
try:
self.volume_info = pytsk3.Volume_Info(self.img_info)
except Exception:
self.volume_info = None
# Attempt to detect a filesystem directly if no volume info
try:
self.fs_info = pytsk3.FS_Info(self.img_info)
except Exception:
self.fs_info = None
# If no volume info and no filesystem, mark as wiped
self.is_wiped_image = True
except Exception as e:
logger.error(f"Error loading image: {e}")
self.img_info = None
self.volume_info = None
self.fs_info = None
self.is_wiped_image = True
def has_filesystem(self, start_offset):
fs_info = self.get_fs_info(start_offset)
return fs_info is not None
def is_wiped(self):
# Image is considered wiped if no volume info, no filesystem detected
return self.is_wiped_image
@property
def partitions(self):
"""Get partitions with caching."""
if self._partition_cache is None:
self._partition_cache = self._get_partitions()
return self._partition_cache
def get_partitions(self):
"""Retrieve partitions from the loaded image, or indicate unpartitioned space."""
return self.partitions
def _get_partitions(self):
"""Internal method to actually retrieve partitions."""
partitions = []
if self.volume_info:
for partition in self.volume_info:
if not partition.desc:
continue
partitions.append((partition.addr, partition.desc, partition.start, partition.len))
return partitions
@lru_cache(maxsize=32)
def get_fs_info(self, start_offset):
"""Retrieve the FS_Info for a partition, initializing it if necessary."""
if start_offset not in self.fs_info_cache:
try:
fs_info = pytsk3.FS_Info(self.img_info, offset=start_offset * 512)
self.fs_info_cache[start_offset] = fs_info
except Exception as e:
return None
return self.fs_info_cache[start_offset]
@lru_cache(maxsize=32)
def get_fs_type(self, start_offset):
"""Retrieve the file system type for a partition."""
try:
fs_type = self.get_fs_info(start_offset).info.ftype
# Map the file system type to its name
fs_type_map = {
pytsk3.TSK_FS_TYPE_NTFS: "NTFS",
pytsk3.TSK_FS_TYPE_FAT12: "FAT12",
pytsk3.TSK_FS_TYPE_FAT16: "FAT16",
pytsk3.TSK_FS_TYPE_FAT32: "FAT32",
pytsk3.TSK_FS_TYPE_EXFAT: "ExFAT",
pytsk3.TSK_FS_TYPE_EXT2: "Ext2",
pytsk3.TSK_FS_TYPE_EXT3: "Ext3",
pytsk3.TSK_FS_TYPE_EXT4: "Ext4",
pytsk3.TSK_FS_TYPE_ISO9660: "ISO9660",
pytsk3.TSK_FS_TYPE_HFS: "HFS",
pytsk3.TSK_FS_TYPE_APFS: "APFS"
}
return fs_type_map.get(fs_type, "Unknown")
except Exception as e:
return "N/A"
def check_partition_contents(self, partition_start_offset):
"""Check if a partition has any files or folders."""
fs = self.get_fs_info(partition_start_offset)
if fs:
try:
root_dir = fs.open_dir(path="/")
for _ in root_dir:
return True
return False
except:
return False
return False
def get_directory_contents(self, start_offset, inode_number=None):
"""Get directory contents with caching for performance."""
cache_key = f"{start_offset}_{inode_number}"
# Check if we have this directory in our cache
if cache_key in self._directory_cache:
return self._directory_cache[cache_key]
fs = self.get_fs_info(start_offset)
if fs:
try:
directory = fs.open_dir(inode=inode_number) if inode_number else fs.open_dir(path="/")
entries = []
for entry in directory:
if entry.info.name.name in [b".", b".."]:
continue
is_directory = False
if entry.info.meta and entry.info.meta.type == pytsk3.TSK_FS_META_TYPE_DIR:
is_directory = True
entries.append({
"name": entry.info.name.name.decode('utf-8', errors='replace') if hasattr(entry.info.name,
'name') else None,
"is_directory": is_directory,
"inode_number": entry.info.meta.addr if entry.info.meta else None,
"size": entry.info.meta.size if entry.info.meta and entry.info.meta.size is not None else 0,
"accessed": safe_datetime(entry.info.meta.atime) if hasattr(entry.info.meta,
'atime') else "N/A",
"modified": safe_datetime(entry.info.meta.mtime) if hasattr(entry.info.meta,
'mtime') else "N/A",
"created": safe_datetime(entry.info.meta.crtime) if hasattr(entry.info.meta,
'crtime') else "N/A",
"changed": safe_datetime(entry.info.meta.ctime) if hasattr(entry.info.meta, 'ctime') else "N/A",
})
# Cache results
self._directory_cache[cache_key] = entries
return entries
except Exception as e:
# Log the exception for debugging purposes
logger.error(f"Error in get_directory_contents: {e}")
return []
return []
def get_registry_hive(self, fs_info, hive_path):
"""Extract a registry hive from the given filesystem."""
try:
registry_file = fs_info.open(hive_path)
hive_data = registry_file.read_random(0, registry_file.info.meta.size)
return hive_data
except Exception as e:
logger.error(f"Error reading registry hive: {e}")
return None
def get_windows_version(self, start_offset):
"""Get the Windows version from the SOFTWARE registry hive."""
fs_info = self.get_fs_info(start_offset)
if not fs_info:
return None
# if file system is not ntfs, return unknown OS and exit the function
if self.get_fs_type(start_offset) != "NTFS":
return None
software_hive_data = self.get_registry_hive(fs_info, "/Windows/System32/config/SOFTWARE")
if not software_hive_data:
return None
# Use a context manager to handle the temporary file
with FileSystemUtils.temp_file() as temp_hive_path:
try:
with open(temp_hive_path, 'wb') as temp_hive:
temp_hive.write(software_hive_data)
reg = Registry.Registry(temp_hive_path)
key = reg.open("Microsoft\\Windows NT\\CurrentVersion")
# Helper function to safely get registry values
def get_reg_value(reg_key, value_name):
try:
return reg_key.value(value_name).value()
except Registry.RegistryValueNotFoundException:
return "N/A"
# Fetching registry values
product_name = get_reg_value(key, "ProductName")
current_version = get_reg_value(key, "CurrentVersion")
current_build = get_reg_value(key, "CurrentBuild")
registered_owner = get_reg_value(key, "RegisteredOwner")
csd_version = get_reg_value(key, "CSDVersion")
product_id = get_reg_value(key, "ProductId")
return f"{product_name} Version {current_version}\nBuild {current_build} {csd_version}\nOwner: {registered_owner}\nProduct ID: {product_id}"
except Exception as e:
logger.error(f"Error parsing SOFTWARE hive: {e}")
return "Error in parsing OS version"
def read_unallocated_space(self, start_offset, end_offset):
try:
start_byte_offset = start_offset * SECTOR_SIZE
end_byte_offset = max(end_offset * SECTOR_SIZE, start_byte_offset + SECTOR_SIZE - 1)
size_in_bytes = end_byte_offset - start_byte_offset + 1 # Ensuring at least some data is read
if size_in_bytes <= 0:
logger.warning("Invalid size for unallocated space, adjusting to read at least one sector.")
size_in_bytes = SECTOR_SIZE # Adjust to read at least one sector
# For large blocks, read in chunks instead of all at once
if size_in_bytes > CHUNK_SIZE:
chunks = []
for offset in range(start_byte_offset, end_byte_offset, CHUNK_SIZE):
remaining = min(CHUNK_SIZE, end_byte_offset - offset + 1)
chunk = self.img_info.read(offset, remaining)
if not chunk:
break
chunks.append(chunk)
if not chunks:
return None
return b''.join(chunks)
else:
unallocated_space = self.img_info.read(start_byte_offset, size_in_bytes)
if unallocated_space is None or len(unallocated_space) == 0:
logger.error(f"Failed to read unallocated space from offset {start_byte_offset} to {end_byte_offset}")
return None
return unallocated_space
except Exception as e:
logger.error(f"Error reading unallocated space: {e}")
return None
def open_image(self):
if self.get_image_type() == "ewf":
filenames = pyewf.glob(self.image_path)
ewf_handle = pyewf.handle()
ewf_handle.open(filenames)
return EWFImgInfo(ewf_handle)
else:
return pytsk3.Img_Info(self.image_path)
def list_files(self, extensions=None):
"""Get a list of all files with given extensions."""
files_list = []
img_info = self.open_image()
try:
volume_info = pytsk3.Volume_Info(img_info)
for partition in volume_info:
if partition.flags == pytsk3.TSK_VS_PART_FLAG_ALLOC:
# Store offset in SECTORS (not bytes)
self.process_partition(img_info, partition.start, files_list, extensions)
except IOError:
self.process_partition(img_info, 0, files_list, extensions)
return files_list
def process_partition(self, img_info, offset_sectors, files_list, extensions):
"""Process partition listing - offset_sectors is in sectors, not bytes."""
try:
fs_info = pytsk3.FS_Info(img_info, offset=offset_sectors * SECTOR_SIZE)
self._recursive_file_search(fs_info, fs_info.open_dir(path="/"), "/", files_list, extensions, None, offset_sectors)
except IOError as e:
logger.error(f"Unable to open filesystem at offset {offset_sectors}: {e}")
def _recursive_file_search(self, fs_info, directory, parent_path, files_list, extensions, search_query=None, start_offset=0):
"""Recursively search for files in a directory."""
for entry in directory:
if entry.info.name.name in [b".", b".."]:
continue
try:
file_name = entry.info.name.name.decode("utf-8", errors='replace')
file_extension = os.path.splitext(file_name)[1].lower()
# Determine if this entry should be included in results
is_directory = entry.info.meta and entry.info.meta.type == pytsk3.TSK_FS_META_TYPE_DIR
if search_query:
# If there's a search query, check if the file name contains the query
if search_query.startswith('.'):
# If the search query is an extension (e.g., '.jpg')
query_matches = file_extension == search_query.lower()
match_reason = f"extension matches '{search_query}'" if query_matches else ""
else:
# If the search query is a file name or part of it (SUBSTRING MATCH)
query_matches = search_query.lower() in file_name.lower()
match_reason = f"filename contains '{search_query}'" if query_matches else ""
else:
# If no search query, handle based on extensions
if is_directory:
# Always include directories when no search query (for navigation)
query_matches = True
match_reason = "directory (no filter)"
else:
# For files, apply extension filter
query_matches = extensions is None or file_extension in extensions or '' in extensions
match_reason = "extension filter"
if is_directory:
# If directory matches search query, add it to results
if query_matches:
dir_info = self._get_directory_metadata(entry, parent_path, start_offset)
files_list.append(dir_info)
if logger.isEnabledFor(logging.DEBUG):
logger.debug(f"MATCH (DIR): '{file_name}' - {match_reason}")
# Recursively search subdirectory
try:
sub_directory = fs_info.open_dir(inode=entry.info.meta.addr)
self._recursive_file_search(fs_info, sub_directory, os.path.join(parent_path, file_name),
files_list,
extensions, search_query, start_offset)
except IOError as e:
logger.error(f"Unable to open directory: {e}")
elif entry.info.meta and entry.info.meta.type == pytsk3.TSK_FS_META_TYPE_REG and query_matches:
file_info = self._get_file_metadata(entry, parent_path, start_offset)
files_list.append(file_info)
if logger.isEnabledFor(logging.DEBUG):
logger.debug(f"MATCH (FILE): '{file_name}' - {match_reason}")
except UnicodeDecodeError:
continue # Skip entries with encoding issues
def _get_directory_metadata(self, entry, parent_path, start_offset=0):
"""Get directory metadata for search results."""
try:
dir_name = entry.info.name.name.decode("utf-8", errors='replace')
inode_number = entry.info.meta.addr if entry.info.meta else 0
# Get volume name for this offset
volume_name = self._get_volume_name_for_offset(start_offset)
# Create full path with volume information
full_path = f"{volume_name}:{os.path.join(parent_path, dir_name)}"
return {
"name": dir_name,
"path": full_path,
"size": 0, # Directories don't have a size in this context
"accessed": safe_datetime(entry.info.meta.atime if entry.info.meta else None),
"modified": safe_datetime(entry.info.meta.mtime if entry.info.meta else None),
"created": safe_datetime(entry.info.meta.crtime if hasattr(entry.info.meta, 'crtime') else None),
"changed": safe_datetime(entry.info.meta.ctime if entry.info.meta else None),
"inode_item": str(inode_number),
"inode_number": inode_number,
"start_offset": start_offset,
"is_directory": True, # Mark as directory
"type": "directory"
}
except Exception as e:
logger.error(f"Error getting directory metadata: {e}")
return {
"name": "Error reading directory",
"path": parent_path + "/unknown",
"size": 0,
"accessed": "N/A",
"modified": "N/A",
"created": "N/A",
"changed": "N/A",
"inode_item": "0",
"inode_number": 0,
"start_offset": start_offset,
"is_directory": True,
"type": "directory"
}
def _get_volume_name_for_offset(self, start_offset):
"""Get the volume name (e.g., 'vol0', 'vol1') for a given partition offset."""
try:
partitions = self.get_partitions()
for addr, desc, start, length in partitions:
if start == start_offset:
return f"vol{addr}"
# If not found in partitions, it might be a single filesystem image
return "vol0"
except Exception as e:
logger.warning(f"Could not determine volume name for offset {start_offset}: {e}")
return "vol0"
def _get_file_metadata(self, entry, parent_path, start_offset=0):
"""Get file metadata including all fields needed for viewing."""
try:
file_name = entry.info.name.name.decode("utf-8", errors='replace')
inode_number = entry.info.meta.addr if entry.info.meta else 0
# Get volume name for this offset
volume_name = self._get_volume_name_for_offset(start_offset)
# Create full path with volume information
full_path = f"{volume_name}:{os.path.join(parent_path, file_name)}"
return {
"name": file_name,
"path": full_path, # Now includes volume information
"size": entry.info.meta.size if entry.info.meta else 0,
"accessed": safe_datetime(entry.info.meta.atime if entry.info.meta else None),
"modified": safe_datetime(entry.info.meta.mtime if entry.info.meta else None),
"created": safe_datetime(entry.info.meta.crtime if hasattr(entry.info.meta, 'crtime') else None),
"changed": safe_datetime(entry.info.meta.ctime if entry.info.meta else None),
"inode_item": str(inode_number), # For display compatibility
"inode_number": inode_number, # For file content retrieval
"start_offset": start_offset, # Partition offset needed for retrieval
"is_directory": False, # This method only called for files
"type": "file" # For compatibility with viewer logic
}
except Exception as e:
logger.error(f"Error getting file metadata: {e}")
# Return basic info when we encounter errors
return {
"name": "Error reading file",
"path": parent_path + "/unknown",
"size": 0,
"accessed": "N/A",
"modified": "N/A",
"created": "N/A",
"changed": "N/A",
"inode_item": "0",
"inode_number": 0,
"start_offset": start_offset,
"is_directory": False,
"type": "file"
}
def search_files(self, search_query=None):
logger.info(f"ImageHandler.search_files called with query: '{search_query}'")
files_list = []
img_info = self.open_image()
try:
volume_info = pytsk3.Volume_Info(img_info)
partition_count = 0
for partition in volume_info:
if partition.flags == pytsk3.TSK_VS_PART_FLAG_ALLOC:
partition_count += 1
logger.info(f"Searching partition {partition_count} (offset: {partition.start} sectors)")
# Store offset in SECTORS (not bytes) - get_fs_info will multiply by 512
self.process_partition_search(img_info, partition.start, files_list, search_query)
logger.info(f"Searched {partition_count} allocated partitions")
except IOError as e:
# No volume information, attempt to read as a single filesystem
logger.info(f"No volume info, reading as single filesystem: {e}")
self.process_partition_search(img_info, 0, files_list, search_query)
logger.info(f"Total files found: {len(files_list)}")
return files_list
def process_partition_search(self, img_info, offset_sectors, files_list, search_query):
"""Process partition search - offset_sectors is in sectors, not bytes."""
try:
logger.info(f"Opening filesystem at offset {offset_sectors} sectors ({offset_sectors * SECTOR_SIZE} bytes)")
fs_info = pytsk3.FS_Info(img_info, offset=offset_sectors * SECTOR_SIZE)
logger.info(f"Starting recursive search with query: '{search_query}'")
initial_count = len(files_list)
self._recursive_file_search(fs_info, fs_info.open_dir(path="/"), "/", files_list, None, search_query, offset_sectors)
logger.info(f"Recursive search complete. Found {len(files_list) - initial_count} files in this partition")
except IOError as e:
logger.error(f"Unable to open file system for search: {e}")
def get_file_content(self, inode_number, offset):
fs = self.get_fs_info(offset)
if not fs:
return None, None
try:
file_obj = fs.open_meta(inode=inode_number)
if file_obj.info.meta.size == 0:
logger.info("File has no content or is a special metafile!")
return None, None
# For large files, read in chunks
file_size = file_obj.info.meta.size
if file_size > CHUNK_SIZE:
chunks = []
for chunk_offset in range(0, file_size, CHUNK_SIZE):
chunk_size = min(CHUNK_SIZE, file_size - chunk_offset)
chunk = file_obj.read_random(chunk_offset, chunk_size)
if not chunk:
break
chunks.append(chunk)
content = b''.join(chunks)
else:
# Small file, read all at once
content = file_obj.read_random(0, file_size)
metadata = file_obj.info.meta # Collect the metadata
return content, metadata
except Exception as e:
logger.error(f"Error reading file: {e}")
return None, None
# Replace static method assignment with an actual instance method
def get_readable_size(self, size_in_bytes):
"""Convert bytes to a human-readable string, wrapper for the static utility method."""
return FileSystemUtils.get_readable_size(size_in_bytes)
# DatabaseManager class with optimization
class DatabaseManager:
def __init__(self, db_path):
self.db_path = db_path
self.db_conn = None
self._icon_cache = {} # Cache for icon paths
self._connect()
def _connect(self):
"""Establish a connection to the database with proper error handling."""
try:
self.db_conn = sqlite3_connect(self.db_path)
# Enable foreign keys
self.db_conn.execute("PRAGMA foreign_keys = ON")
except Exception as e:
logger.error(f"Error connecting to database: {e}")
self.db_conn = None
def __del__(self):
"""Ensure connection is closed when object is destroyed."""
self.close()
def close(self):
"""Explicitly close the database connection."""
if self.db_conn:
try:
self.db_conn.close()
self.db_conn = None
except Exception as e:
logger.error(f"Error closing database connection: {e}")
def get_icon_path(self, icon_type, identifier):
"""Get icon path with caching for performance."""
# Check cache first
cache_key = f"{icon_type}_{identifier}"
if cache_key in self._icon_cache:
return self._icon_cache[cache_key]
if not self.db_conn:
self._connect()
if not self.db_conn:
return 'Icons/mimetypes/application-x-zerosize.svg'
try:
c = self.db_conn.cursor()
# First, try to get the icon for the specific identifier
c.execute("SELECT path FROM icons WHERE type = ? AND extention = ?", (icon_type, identifier))
result = c.fetchone()
# If a specific icon exists for the identifier, cache and return it
if result:
self._icon_cache[cache_key] = result[0]
return result[0]
# If no specific icon exists, check for default icons
if icon_type == 'folder':
c.execute("SELECT path FROM icons WHERE type = ? AND extention = 'folder'", (icon_type,))
result = c.fetchone()
default_path = result[0] if result else 'Icons/mimetypes/application-x-zerosize.svg'
else:
# Try to find a generic icon for the file type first
generic_key = f"{icon_type}_generic"
if generic_key not in self._icon_cache:
c.execute("SELECT path FROM icons WHERE type = ? AND extention = 'generic'", (icon_type,))
result = c.fetchone()
self._icon_cache[generic_key] = result[
0] if result else 'Icons/mimetypes/application-x-zerosize.svg'
default_path = self._icon_cache[generic_key]
# Cache the result before returning
self._icon_cache[cache_key] = default_path
return default_path
except Exception as e:
logger.error(f"Error fetching icon: {e}")
return 'Icons/mimetypes/application-x-zerosize.svg'
finally:
if 'c' in locals():
c.close()
# ImageManager class with optimizations
class ImageManager(QThread):
operationCompleted = Signal(bool, str) # Signal to indicate operation completion
showMessage = Signal(str, str) # Signal to show a message (Title, Content)
progressUpdated = Signal(int) # Signal for progress updates
def __init__(self):
super().__init__()
self.operation = None
self.image_path = None
self.file_name = None
self.is_running = False
self._process = None
def __del__(self):
self.cleanup_resources()
def cleanup_resources(self):
"""Clean up any resources used by the image mounting process."""
if self._process and hasattr(self._process, 'poll') and self._process.poll() is None:
try:
self._process.terminate()
self._process = None
except:
pass
def run(self):
self.is_running = True
system = platform.system()
try:
if self.operation == 'mount' and self.image_path:
if system == 'Darwin': # macOS
self._mount_image_macos()
elif system == 'Linux': # Linux (including Kali)
self._mount_image_linux()
elif system == 'Windows': # Windows
self._mount_image_windows()
else:
raise Exception("Unsupported Operating System")
elif self.operation == 'dismount':
if system == 'Darwin':
self._dismount_image_macos()
elif system == 'Linux':
self._dismount_image_linux()
elif system == 'Windows':
self._dismount_image_windows()
else:
raise Exception("Unsupported Operating System")
except Exception as e:
self.operationCompleted.emit(False, f"Failed to {self.operation} the image. Error: {e}")
finally:
self.is_running = False
def _mount_image_windows(self):
"""Mount image on Windows using Arsenal Image Mounter."""
try:
aim_path = 'tools/Arsenal-Image-Mounter-v3.10.257/aim_cli.exe'
if not os.path.exists(aim_path):
self.operationCompleted.emit(False, "Arsenal Image Mounter not found. Please install it.")
return
cmd = [
aim_path,
'--mount',
'--readonly',
f'--filename={self.image_path}'
]
# Use subprocess.Popen with proper parameter checking
self._process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0
)
# Wait for the process to complete or timeout after 30 seconds
try:
stdout, stderr = self._process.communicate(timeout=MOUNT_TIMEOUT)
if self._process.returncode != 0:
error_msg = stderr.decode('utf-8', errors='replace')
self.operationCompleted.emit(False, f"Failed to mount the image: {error_msg}")
return
self.operationCompleted.emit(True, f"Image {self.file_name} mounted successfully.")
except subprocess.TimeoutExpired:
# Process is taking too long, but this is sometimes normal for mounting
# We'll assume it's working in the background
self.operationCompleted.emit(True,
f"Image {self.file_name} mount initiated. Check Windows Disk Management.")
except Exception as e:
self.operationCompleted.emit(False, f"Failed to mount the image on Windows. Error: {e}")
def _mount_image_macos(self):
"""Mount image on macOS using hdiutil."""
try:
# Step 1: Attach the image without mounting it
attach_cmd = [
'hdiutil', 'attach',
'-imagekey', 'diskimage-class=CRawDiskImage',
'-nomount', self.image_path
]
attach_process = subprocess.Popen(
attach_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
# Wait with timeout
try:
attach_output, _ = attach_process.communicate(timeout=MOUNT_TIMEOUT)
if attach_process.returncode != 0:
self.operationCompleted.emit(False, f"Failed to attach image: {attach_output.decode()}")
return
except subprocess.TimeoutExpired:
attach_process.kill()
self.operationCompleted.emit(False, "Attaching image timed out")
return
attach_output = attach_output.decode().strip()
# Step 2: Add a short delay to ensure the system has time to process the attachment
QThread.msleep(THREAD_SLEEP_MS) # More reliable than time.sleep in a QThread
# Step 3: Extract the disk identifier from the output
lines = attach_output.splitlines()
disk_identifier = None
for line in lines:
if line.startswith('/dev/disk'):
disk_identifier = line.split()[0]
break
if not disk_identifier:
self.operationCompleted.emit(False, "Failed to find disk identifier after attaching the image.")
return
# Step 4: Mount the disk using the identifier
mount_cmd = ['hdiutil', 'mount', disk_identifier]
mount_process = subprocess.Popen(
mount_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
try:
mount_output, _ = mount_process.communicate(timeout=MOUNT_TIMEOUT)
if mount_process.returncode != 0:
self.operationCompleted.emit(False, f"Failed to mount disk: {mount_output.decode()}")
return
except subprocess.TimeoutExpired:
mount_process.kill()
self.operationCompleted.emit(False, "Mounting timed out")
return
mount_output = mount_output.decode().strip()
# Step 5: Extract the mount point (e.g., /Volumes/LABEL2)
lines = mount_output.splitlines()
mount_point = None
for line in lines:
if line.startswith('/dev/') and '\t' in line:
mount_point = line.split('\t')[1]
break
if mount_point:
# Emit success with the mount point
self.operationCompleted.emit(True, f"Image {self.file_name} mounted successfully at {mount_point}.")
else:
self.operationCompleted.emit(False, f"Image {self.file_name} mounted, but no volumes were detected.")
except subprocess.CalledProcessError as e:
self.operationCompleted.emit(False, f"Failed to mount the image on macOS. Error: {e.output.decode()}")
except Exception as e:
self.operationCompleted.emit(False, f"Unexpected error mounting image: {str(e)}")
def _mount_image_linux(self):
"""Mount image on Linux using appropriate tools."""
try:
if self.image_path.lower().endswith('.e01'):
# Use ewfmount for .e01 images
ewf_mount_dir = '/mnt/ewf'
# Create mount directory if it doesn't exist
if not os.path.exists(ewf_mount_dir):
os.makedirs(ewf_mount_dir, exist_ok=True)
# Run ewfmount with proper error handling
ewf_cmd = ['sudo', 'ewfmount', self.image_path, ewf_mount_dir]
ewf_process = subprocess.run(ewf_cmd, check=True, capture_output=True, text=True)
# Get the partition table info using fdisk
fdisk_cmd = ['fdisk', '-l', os.path.join(ewf_mount_dir, 'ewf1')]
fdisk_output = subprocess.check_output(fdisk_cmd, text=True)
# Find the partition start sector
partition_start_sector = None
for line in fdisk_output.splitlines():
if '/dev/' in line and not line.startswith('Disk '):
# Assuming you want the first partition listed
parts = line.split()
if len(parts) > 1:
try:
partition_start_sector = int(parts[1])
break
except (ValueError, IndexError):
continue
if partition_start_sector is None:
raise Exception("Failed to find partition start sector in the EWF image.")
# Calculate the byte offset
byte_offset = partition_start_sector * 512
# Mount the partition using the calculated offset
mount_dir = '/mnt/disk_image'
os.makedirs(mount_dir, exist_ok=True)
mount_cmd = [
'sudo', 'mount', '-o',
f'ro,loop,offset={byte_offset}',
os.path.join(ewf_mount_dir, 'ewf1'),
mount_dir
]
mount_process = subprocess.run(mount_cmd, check=True, capture_output=True, text=True)
else:
# Use mount for .dd images and other raw formats
mount_dir = '/mnt/disk_image'
os.makedirs(mount_dir, exist_ok=True)
mount_cmd = [
'sudo', 'mount', '-o', 'loop,ro',
self.image_path, mount_dir
]
mount_process = subprocess.run(mount_cmd, check=True, capture_output=True, text=True)
self.operationCompleted.emit(True, f"Image {self.file_name} mounted successfully.")
except subprocess.CalledProcessError as e:
self.operationCompleted.emit(False, f"Failed to mount the image on Linux. Error: {e.stderr}")
except Exception as e:
self.operationCompleted.emit(False, f"An unexpected error occurred: {str(e)}")
def _dismount_image_linux(self):
"""Dismount image on Linux."""
try:
# Attempt to unmount the disk image
disk_cmd = ['sudo', 'umount', '/mnt/disk_image']
ewf_cmd = ['sudo', 'umount', '/mnt/ewf']
try:
# Try to unmount disk image
subprocess.run(disk_cmd, check=True, capture_output=True, text=True)
except subprocess.CalledProcessError as e:
logger.warning(f"Could not unmount disk image: {e.stderr}")
try:
# Try to unmount EWF
subprocess.run(ewf_cmd, check=True, capture_output=True, text=True)
except subprocess.CalledProcessError as e:
logger.warning(f"Could not unmount EWF: {e.stderr}")
self.operationCompleted.emit(True, "Image was dismounted successfully.")
except Exception as e:
self.operationCompleted.emit(False, f"Failed to dismount the image on Linux. Error: {str(e)}")
def _dismount_image_macos(self):
"""Dismount image on macOS using hdiutil."""
try:
# Get the list of currently mounted disk images
info_cmd = ['hdiutil', 'info']
info_process = subprocess.Popen(
info_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT
)
try:
info_output, _ = info_process.communicate(timeout=INFO_TIMEOUT)
if info_process.returncode != 0:
self.operationCompleted.emit(False, f"Failed to get mounted disks: {info_output.decode()}")
return
except subprocess.TimeoutExpired:
info_process.kill()
self.operationCompleted.emit(False, "Getting disk info timed out")
return
info_output = info_output.decode()
lines = info_output.splitlines()
mounted_disks = []
current_image_path = None
# Parse the output to find the disk identifier for the given image path
for line in lines:
if 'image-path' in line:
current_image_path = line.split(': ')[1].strip()
elif line.startswith('/dev/disk') and current_image_path == self.image_path:
disk_identifier = line.split()[0]
mounted_disks.append(disk_identifier)
current_image_path = None # Reset after finding the corresponding disk
if not mounted_disks:
# If we're not targeting a specific image, try to unmount all mounted disks
if not self.image_path:
for line in lines:
if line.startswith('/dev/disk'):
disk_identifier = line.split()[0]
mounted_disks.append(disk_identifier)
if not mounted_disks:
self.operationCompleted.emit(False, "No mounted images found.")
return
# Attempt to dismount all found disk identifiers
success = False
errors = []
for disk_identifier in mounted_disks:
try:
detach_cmd = ['hdiutil', 'detach', disk_identifier]
detach_process = subprocess.run(detach_cmd, check=True, capture_output=True, text=True)
success = True
except subprocess.CalledProcessError:
try:
# If normal detach fails, attempt a forced detach
force_detach_cmd = ['hdiutil', 'detach', '-force', disk_identifier]
force_process = subprocess.run(force_detach_cmd, check=True, capture_output=True, text=True)
success = True
except subprocess.CalledProcessError as e:
errors.append(f"Failed to detach {disk_identifier}: {e.stderr}")
if success:
self.operationCompleted.emit(True, "Image was dismounted successfully.")
else:
self.operationCompleted.emit(False, "Failed to dismount all images: " + "; ".join(errors))
except Exception as e:
self.operationCompleted.emit(False, f"Failed to dismount the image on macOS: {str(e)}")
def _dismount_image_windows(self):
"""Dismount image on Windows using Arsenal Image Mounter."""
try:
aim_path = 'tools/Arsenal-Image-Mounter-v3.10.257/aim_cli.exe'
if not os.path.exists(aim_path):
self.operationCompleted.emit(False, "Arsenal Image Mounter not found. Please install it.")
return
cmd = [aim_path, '--dismount']
# Use subprocess.run with proper error handling
process = subprocess.run(
cmd,
check=True,
capture_output=True,
text=True,
creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0
)
self.operationCompleted.emit(True, "Image was dismounted successfully.")
except subprocess.CalledProcessError as e:
self.operationCompleted.emit(False, f"Failed to dismount the image on Windows. Error: {e.stderr}")
except Exception as e:
self.operationCompleted.emit(False, f"Unexpected error dismounting image: {str(e)}")
def dismount_image(self):
"""Attempt to dismount the currently mounted image."""
if self.is_running:
self.showMessage.emit("Operation in Progress", "Please wait for the current operation to complete.")
return
self.operation = 'dismount'
self.start()
def mount_image(self):
"""Attempt to mount an image after prompting the user to select one."""
if self.is_running:
self.showMessage.emit("Operation in Progress", "Please wait for the current operation to complete.")
return
system = platform.system()
if system == 'Darwin': # macOS
# Only allow .raw and .dd files on macOS
supported_formats = "Raw Files (*.raw *.dd);;All Files (*)"
valid_extensions = ['.raw', '.dd']
else:
# Original behavior for other operating systems
supported_formats = (
"EWF Files (*.E01);;Raw Files (*.dd);;AFF4 Files (*.aff4);;"
"VHD Files (*.vhd);;VDI Files (*.vdi);;XVA Files (*.xva);;"
"VMDK Files (*.vmdk);;OVA Files (*.ova);;QCOW Files (*.qcow *.qcow2);;All Files (*)"
)
valid_extensions = ['.e01', '.dd', '.aff4', '.vhd', '.vdi', '.xva', '.vmdk', '.ova', '.qcow', '.qcow2']
while True:
image_path, _ = QFileDialog.getOpenFileName(QWidget(None), "Select Disk Image", "", supported_formats)
if not image_path:
return # No image was selected, so just exit the function
file_extension = os.path.splitext(image_path)[1].lower()
if file_extension in valid_extensions:
break # Exit the loop if a valid image was selected
else:
# Show an error message for an invalid file
QMessageBox.warning(QWidget(None), "Invalid File Type", "The selected file is not a valid disk image.")
# Normalize the path
self.image_path = os.path.normpath(image_path)
self.file_name = os.path.basename(self.image_path)
self.operation = 'mount'
self.start()
# ==================== FILE SEARCH WIDGET CLASSES ====================
class SizeTableWidgetItem(QTableWidgetItem):
"""Custom table widget item for proper size sorting."""
def __lt__(self, other):
return int(self.data(Qt.UserRole)) < int(other.data(Qt.UserRole))
class MainWindow(QMainWindow):
# Class variable for icon caching
_icon_cache = {}
def __init__(self):
super().__init__()
# Create a database manager for icon lookup
self.db_manager = DatabaseManager('tools/new_database_mappings.db')
# Initialize variables for tracking
self.current_selected_data = None
self.current_offset = None
self.current_path = "/" # Initialize current path
self.image_handler = None
self._directory_cache = {}
# Search/Browse mode state management
self._search_mode = False # False = Browse mode, True = Search mode
self._search_query = "" # Current search query
self._last_browsed_state = {} # Store last directory state for restoration
# Search debounce timer - wait for user to stop typing before searching
self._search_timer = QTimer()
self._search_timer.setSingleShot(True)
self._search_timer.setInterval(500) # 500ms delay after last keystroke
self._search_timer.timeout.connect(self._execute_search)
# Directory navigation history (for Back/Forward buttons like Windows 11)
self._directory_history = [] # List of visited directories: [(offset, inode, path), ...]
self._history_index = -1 # Current position in history (-1 = no history)
self._navigating_history = False # Flag to prevent adding to history during Back/Forward
# Load configuration
self.api_keys = configparser.ConfigParser()
try:
self.api_keys.read('config.ini')
except Exception as e:
logger.error(f"Error loading configuration: {e}")
# Initialize instance attributes
self.image_mounted = False
self.current_offset = None
self.current_image_path = None
self.image_manager = ImageManager()
self.current_selected_data = None
self.evidence_files = []
# Connect to named method instead of complex lambda
self.image_manager.operationCompleted.connect(self._handle_mount_operation_complete)
self.initialize_ui()
# ==================== HELPER METHODS ====================
def _handle_mount_operation_complete(self, success: bool, message: str) -> None:
"""Handle completion of mount/dismount operation."""
if success:
QMessageBox.information(self, "Image Operation", message)
self.image_mounted = not self.image_mounted
else:
QMessageBox.critical(self, "Image Operation", message)
def _get_file_icon(self, file_extension: str) -> QIcon:
"""Get icon for file extension with caching."""
if file_extension not in self._icon_cache:
icon_path = self.db_manager.get_icon_path('file', file_extension)
self._icon_cache[file_extension] = QIcon(icon_path)
return self._icon_cache[file_extension]
def _format_partition_text(self, addr: int, desc: bytes, start: int, end: int, length: int, fs_type: str) -> str:
"""Format partition display text."""
size_in_bytes = length * SECTOR_SIZE
readable_size = self.image_handler.get_readable_size(size_in_bytes)
desc_str = desc.decode('utf-8') if isinstance(desc, bytes) else desc
return f"vol{addr} ({desc_str}: {start}-{end}, Size: {readable_size}, FS: {fs_type})"
def _confirm_exit(self) -> bool:
"""Ask user to confirm exit."""
reply = QMessageBox.question(
self, 'Exit Confirmation',
'Are you sure you want to exit?',
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
return reply == QMessageBox.StandardButton.Yes
def _handle_dismount_if_needed(self) -> None:
"""Dismount image if mounted and user confirms."""
if not self.image_mounted:
return
reply = QMessageBox.question(
self, 'Dismount Image',
'Do you want to dismount the mounted image before exiting?',
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.Yes
)
if reply == QMessageBox.StandardButton.Yes:
self.image_manager.dismount_image()
def _create_tree_item_for_entry(self, parent_item: QTreeWidgetItem, entry: Dict[str, Any],
start_offset: int) -> QTreeWidgetItem:
"""Create tree item for a directory entry."""
child_item = QTreeWidgetItem(parent_item)
child_item.setText(0, entry["name"])
if entry["is_directory"]:
self._setup_directory_tree_item(child_item, entry, start_offset)
else:
self._setup_file_tree_item(child_item, entry, start_offset)
return child_item
def _setup_directory_tree_item(self, item: QTreeWidgetItem, entry: Dict[str, Any],
start_offset: int) -> None:
"""Configure tree item for a directory entry."""
# Check if directory has children
sub_entries = self.image_handler.get_directory_contents(start_offset, entry["inode_number"])
has_sub_entries = bool(sub_entries)
# Set directory icon and data
icon_path = self.db_manager.get_icon_path('folder', 'folder')
item.setIcon(0, QIcon(icon_path))
item.setData(0, Qt.UserRole, {
"inode_number": entry["inode_number"],
"type": 'directory',
"start_offset": start_offset,
"name": entry["name"]
})
# Set child indicator
item.setChildIndicatorPolicy(
QTreeWidgetItem.ShowIndicator if has_sub_entries
else QTreeWidgetItem.DontShowIndicatorWhenChildless
)
def _setup_file_tree_item(self, item: QTreeWidgetItem, entry: Dict[str, Any],
start_offset: int) -> None:
"""Configure tree item for a file entry."""
# Get file extension for icon
file_extension = entry["name"].split('.')[-1].lower() if '.' in entry["name"] else 'unknown'
# Use cached icon lookup
icon = self._get_file_icon(file_extension)
item.setIcon(0, icon)
item.setData(0, Qt.UserRole, {
"inode_number": entry["inode_number"],
"type": 'file',
"start_offset": start_offset,
"name": entry["name"]
})
def _populate_table_entry(self, row_position: int, entry: Dict[str, Any], offset: int) -> None:
"""Populate a single table row with entry data."""
entry_name = entry.get("name", "")
inode_number = entry.get("inode_number", 0)
is_directory = entry.get("is_directory", False)
description = "Dir" if is_directory else "File"
size_in_bytes = entry.get("size", 0)
readable_size = self.image_handler.get_readable_size(size_in_bytes)
created = entry.get("created", "N/A")
accessed = entry.get("accessed", "N/A")
modified = entry.get("modified", "N/A")
changed = entry.get("changed", "N/A")
icon_type = 'folder' if is_directory else 'file'
icon_name = 'folder' if is_directory else (
entry_name.split('.')[-1].lower() if '.' in entry_name else 'unknown')
parent_inode = self.current_selected_data.get("inode_number") if self.current_selected_data else None
self.listing_table.insertRow(row_position)
self.insert_row_into_listing_table(entry_name, inode_number, description,
icon_name, icon_type, offset,
readable_size, created, accessed,
modified, changed, parent_inode)
# ==================== END HELPER METHODS ====================
def initialize_ui(self):
self.setWindowTitle('Trace 1.2.0')
# Set application icon for all platforms
app_icon = QIcon('Icons/logo_prev_ui.png')
self.setWindowIcon(app_icon)
# Set taskbar/dock icon for different platforms
if os.name == 'nt': # Windows
import ctypes
myappid = 'Trace'
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
else: # macOS and Linux
# For macOS and Linux, setting the app icon at application level
QApplication.instance().setWindowIcon(app_icon)
self.setGeometry(DEFAULT_WINDOW_X, DEFAULT_WINDOW_Y, DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)
menu_bar = QMenuBar(self)
file_actions = {
'Add Evidence File': self.load_image_evidence,
'Remove Evidence File': self.remove_image_evidence,
'Image Mounting': self.image_manager.mount_image,
'Image Unmounting': self.image_manager.dismount_image,
'separator': None, # This will add a separator
'Exit': self.close
}
self.create_menu(menu_bar, 'File', file_actions)
view_menu = QMenu('View', self)
# Create the "Full Screen" action and connect it to the showFullScreen slot
full_screen_action = QAction("Full Screen", self)
full_screen_action.triggered.connect(self.showFullScreen)
view_menu.addAction(full_screen_action)
# Create the "Normal Screen" action and connect it to the showNormal slot
normal_screen_action = QAction("Normal Screen", self)
normal_screen_action.triggered.connect(self.showNormal)
view_menu.addAction(normal_screen_action)
# Add a separator
view_menu.addSeparator()
# **Add Theme Selection Actions**
# Create an action group for themes
theme_group = QActionGroup(self)
theme_group.setExclusive(True) # Only one theme can be selected at a time
# Light Theme Action
light_theme_action = QAction("Light Mode", self)
light_theme_action.setCheckable(True)
light_theme_action.setChecked(True) # Set Light Theme as default
light_theme_action.triggered.connect(lambda: self.apply_stylesheet('light'))
theme_group.addAction(light_theme_action)
view_menu.addAction(light_theme_action)
# Dark Theme Action
dark_theme_action = QAction("Dark Mode", self)
dark_theme_action.setCheckable(True)
dark_theme_action.triggered.connect(lambda: self.apply_stylesheet('dark'))
theme_group.addAction(dark_theme_action)
view_menu.addAction(dark_theme_action)
# Add the view menu to the menu bar
menu_bar.addMenu(view_menu)
# **Apply the default stylesheet**
self.apply_stylesheet('light')
tools_menu = QMenu('Tools', self)
verify_image_action = QAction("Verify Image", self)
verify_image_action.triggered.connect(self.verify_image)
tools_menu.addAction(verify_image_action)
conversion_action = QAction("Convert E01 to DD/RAW", self)
conversion_action.triggered.connect(self.show_conversion_widget)
tools_menu.addAction(conversion_action)
veriphone_api_action = QAction("Veriphone API", self)
veriphone_api_action.triggered.connect(self.show_veriphone_widget)
tools_menu.addAction(veriphone_api_action)
# Add "Options" menu for API key configuration
options_menu = QMenu('Options', self)
api_key_action = QAction("API Keys", self)
api_key_action.triggered.connect(self.show_api_key_dialog)
options_menu.addAction(api_key_action)
help_menu = QMenu('Help', self)
help_menu.addAction("About")
help_menu.triggered.connect(lambda: AboutDialog(self).exec_())
menu_bar.addMenu(view_menu)
menu_bar.addMenu(tools_menu)
menu_bar.addMenu(options_menu)
menu_bar.addMenu(help_menu)
self.setMenuBar(menu_bar)
self.main_toolbar = QToolBar()
self.main_toolbar.setMovable(False)
self.main_toolbar.setFloatable(False)
self.main_toolbar.addAction(
self.create_action('Icons/icons8-evidence-48.png', "Load Image", self.load_image_evidence))
self.main_toolbar.addAction(
self.create_action('Icons/icons8-evidence-96.png', "Remove Image", self.remove_image_evidence))
self.main_toolbar.addSeparator()
# Create verify_image_button as an attribute of MainWindow
self.verify_image_button = self.create_action('Icons/icons8-verify-blue.png', "Verify Image", self.verify_image)
self.main_toolbar.addAction(self.verify_image_button)
self.main_toolbar.addSeparator()
self.main_toolbar.addAction(
self.create_action('Icons/devices/icons8-hard-disk-48.png', "Mount Image", self.image_manager.mount_image))
self.main_toolbar.addAction(self.create_action('Icons/devices/icons8-hard-disk-48_red.png', "Unmount Image",
self.image_manager.dismount_image))
# Navigation buttons (Back, Forward, Up) will be added to the listing search toolbar
# Created later in the UI setup
self.addToolBar(Qt.TopToolBarArea, self.main_toolbar)
self.tree_viewer = QTreeWidget(self)
self.tree_viewer.setIconSize(QSize(16, 16))
self.tree_viewer.setHeaderHidden(True)
self.tree_viewer.itemExpanded.connect(self.on_item_expanded)
self.tree_viewer.itemClicked.connect(self.on_item_clicked)
self.tree_viewer.setContextMenuPolicy(Qt.CustomContextMenu)
self.tree_viewer.customContextMenuRequested.connect(self.open_tree_context_menu)
tree_dock = QDockWidget('Tree View', self)
tree_dock.setWidget(self.tree_viewer)
self.addDockWidget(Qt.LeftDockWidgetArea, tree_dock)
self.result_viewer = QTabWidget(self)
self.setCentralWidget(self.result_viewer)
self.listing_table = QTableWidget()
self.listing_table.setSortingEnabled(True)
self.listing_table.verticalHeader().setVisible(False)
self.listing_table.setObjectName("listingTable") # Set object name for specific CSS styling
# Set size policy to expand with window
self.listing_table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# Use alternate row colors
self.listing_table.setAlternatingRowColors(True)
self.listing_table.setEditTriggers(QTableWidget.NoEditTriggers)
self.listing_table.setIconSize(QSize(24, 24))
self.listing_table.setColumnCount(10) # 10 columns: Name, Inode, Type, Size, 4 timestamps, Path, Info
# Enable horizontal scrolling for smaller windows
self.listing_table.setHorizontalScrollMode(QTableWidget.ScrollPerPixel)
self.listing_table.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
# Connect click event to handle navigation in search mode
self.listing_table.itemClicked.connect(self.on_listing_table_item_clicked)
# Create a QVBoxLayout for the listing tab
self.listing_layout = QVBoxLayout()
self.listing_layout.setContentsMargins(0, 0, 0, 0) # Set to zero to remove margins
self.listing_layout.setSpacing(0) # Remove spacing between widgets
# ==================== CREATE UNIFIED TOOLBAR (like File Carving tab) ====================
self.listing_toolbar = QToolBar()
self.listing_toolbar.setContentsMargins(0, 0, 0, 0)
self.listing_toolbar.setMovable(False)
# LEFT SIDE: Icon and Title
self.listing_icon_label = QLabel()
self.listing_icon_label.setPixmap(QPixmap('Icons/icons8-search-in-browser-50.png'))
self.listing_icon_label.setFixedSize(48, 48)
self.listing_toolbar.addWidget(self.listing_icon_label)
self.listing_title_label = QLabel("File System Browser")
self.listing_title_label.setStyleSheet("""
QLabel {
font-size: 20px;
color: #37c6d0;
font-weight: bold;
margin-left: 8px;
}
""")
self.listing_toolbar.addWidget(self.listing_title_label)
# Add spacer after title
title_spacer = QLabel()
title_spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
self.listing_toolbar.addWidget(title_spacer)
# MIDDLE: Navigation buttons (Back, Forward, Up) - next to title
self.back_action = QAction(QIcon("Icons/icons8-left-arrow-50.png"), "Back", self)
self.back_action.triggered.connect(self.navigate_back)
self.back_action.setEnabled(False)
self.listing_toolbar.addAction(self.back_action)
self.forward_action = QAction(QIcon("Icons/icons8-right-arrow-50.png"), "Forward", self)
self.forward_action.triggered.connect(self.navigate_forward)
self.forward_action.setEnabled(False)
self.listing_toolbar.addAction(self.forward_action)
self.go_up_action = QAction(QIcon("Icons/icons8-thick-arrow-pointing-up-50.png"), "Go Up Directory", self)
self.go_up_action.triggered.connect(self.navigate_up_directory)
self.go_up_action.setEnabled(False)
self.listing_toolbar.addAction(self.go_up_action)
# Add vertical separator after navigation buttons
self.listing_toolbar.addSeparator()
# RIGHT SIDE: Search functionality
# Add search bar
self.listing_search_bar = QLineEdit()
self.listing_search_bar.setObjectName("listingSearchBar")
self.listing_search_bar.setPlaceholderText("Search files (press Enter, supports wildcards: *.pdf, name.*)")
self.listing_search_bar.setFixedHeight(35)
self.listing_search_bar.setFixedWidth(450)
# Only search when user presses Enter
self.listing_search_bar.returnPressed.connect(self.trigger_listing_search)
# Monitor text changes for auto-clearing results
self.listing_search_bar.textChanged.connect(self.on_listing_search_text_changed)
self.listing_toolbar.addWidget(self.listing_search_bar)
# Add small end spacer
end_spacer = QWidget()
end_spacer.setFixedWidth(10)
self.listing_toolbar.addWidget(end_spacer)
# Add the single toolbar and listing table to the layout
self.listing_layout.addWidget(self.listing_toolbar) # Single unified toolbar
self.listing_layout.addWidget(self.listing_table) # Table below toolbar
# Create a widget to hold the layout
self.listing_widget = QWidget()
self.listing_widget.setLayout(self.listing_layout)
# Set the horizontal header with hybrid resizing approach
header = self.listing_table.horizontalHeader()
# All columns use Interactive mode (fixed width, manually resizable)
# This enables horizontal scrolling on smaller windows
header.setSectionResizeMode(0, QHeaderView.Interactive) # Name - fixed, manually resizable
header.setSectionResizeMode(1, QHeaderView.Interactive) # Inode - fixed, manually resizable
header.setSectionResizeMode(2, QHeaderView.Interactive) # Type - fixed, manually resizable
header.setSectionResizeMode(3, QHeaderView.Interactive) # Size - fixed, manually resizable
header.setSectionResizeMode(4, QHeaderView.Interactive) # Created - fixed, manually resizable
header.setSectionResizeMode(5, QHeaderView.Interactive) # Accessed - fixed, manually resizable
header.setSectionResizeMode(6, QHeaderView.Interactive) # Modified - fixed, manually resizable
header.setSectionResizeMode(7, QHeaderView.Interactive) # Changed - fixed, manually resizable
header.setSectionResizeMode(8, QHeaderView.Interactive) # Path - fixed, manually resizable
header.setSectionResizeMode(9, QHeaderView.Interactive) # Info - fixed, manually resizable
# Set initial column widths
self.listing_table.setColumnWidth(0, COLUMN_WIDTHS['name']) # Name - 400px (widest)
self.listing_table.setColumnWidth(1, COLUMN_WIDTHS['inode']) # Inode - 45px
self.listing_table.setColumnWidth(2, COLUMN_WIDTHS['type']) # Type - 50px
self.listing_table.setColumnWidth(3, COLUMN_WIDTHS['size']) # Size - 70px
self.listing_table.setColumnWidth(4, COLUMN_WIDTHS['created']) # Created - 90px (narrower)
self.listing_table.setColumnWidth(5, COLUMN_WIDTHS['accessed']) # Accessed - 90px (narrower)
self.listing_table.setColumnWidth(6, COLUMN_WIDTHS['modified']) # Modified - 90px (narrower)
self.listing_table.setColumnWidth(7, COLUMN_WIDTHS['changed']) # Changed - 90px (narrower)
self.listing_table.setColumnWidth(8, COLUMN_WIDTHS['path']) # Path - 300px (wide)
self.listing_table.setColumnWidth(9, 250) # Info - 250px (for volumes)
# Remove any extra space in the header
header.setStyleSheet("QHeaderView::section { margin-top: 0px; padding-top: 2px; }")
header.setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter)
# Set the header labels
self.listing_table.setHorizontalHeaderLabels(
['Name', 'Inode', 'Type', 'Size', 'Created Date', 'Accessed Date', 'Modified Date', 'Changed Date', 'Path', 'Info']
)
self.listing_table.itemDoubleClicked.connect(self.on_listing_table_item_clicked)
self.listing_table.setContextMenuPolicy(Qt.CustomContextMenu)
self.listing_table.customContextMenuRequested.connect(self.open_listing_context_menu)
self.listing_table.setSelectionBehavior(QTableWidget.SelectRows)
# Set the color of the selected row
palette = self.listing_table.palette()
palette.setBrush(QPalette.Highlight, QBrush(Qt.lightGray)) # Change Qt.lightGray to your preferred color
self.listing_table.setPalette(palette)
header = self.listing_table.horizontalHeader()
header.setDefaultAlignment(Qt.AlignLeft)
self.result_viewer.addTab(self.listing_widget, 'Listing')
self.deleted_files_widget = FileCarvingWidget(self)
self.result_viewer.addTab(self.deleted_files_widget, 'Deleted Files')
self.registry_extractor_widget = RegistryExtractor(self.image_handler)
self.result_viewer.addTab(self.registry_extractor_widget, 'Registry')
self.viewer_tab = QTabWidget(self)
self.hex_viewer = HexViewer(self)
self.viewer_tab.addTab(self.hex_viewer, 'Hex')
self.text_viewer = TextViewer(self)
self.viewer_tab.addTab(self.text_viewer, 'Text')
self.application_viewer = UnifiedViewer(self)
self.application_viewer.layout.setContentsMargins(0, 0, 0, 0)
self.application_viewer.layout.setSpacing(0)
self.viewer_tab.addTab(self.application_viewer, 'Application')
self.metadata_viewer = MetadataViewer(self.image_handler)
self.viewer_tab.addTab(self.metadata_viewer, 'File Metadata')
self.exif_viewer = ExifViewer(self)
self.viewer_tab.addTab(self.exif_viewer, 'Exif Data')
self.virus_total_api = VirusTotal()
self.viewer_tab.addTab(self.virus_total_api, 'Virus Total API')
# Set the API key if it exists
virus_total_key = self.api_keys.get('API_KEYS', 'virustotal', fallback='')
self.virus_total_api.set_api_key(virus_total_key)
self.viewer_dock = QDockWidget('Utils', self)
self.viewer_dock.setWidget(self.viewer_tab)
self.addDockWidget(Qt.BottomDockWidgetArea, self.viewer_dock)
self.viewer_dock.setMinimumSize(VIEWER_DOCK_MAX_WIDTH, VIEWER_DOCK_MIN_HEIGHT)
self.viewer_dock.setMaximumSize(VIEWER_DOCK_MAX_WIDTH, VIEWER_DOCK_MIN_HEIGHT)
self.viewer_dock.visibilityChanged.connect(self.on_viewer_dock_focus)
self.viewer_tab.currentChanged.connect(self.display_content_for_active_tab)
# disable all tabs before loading an image file
self.enable_tabs(False)
def apply_stylesheet(self, theme='light'):
if theme == 'dark':
qss_file = 'styles/dark_theme.qss'
else:
qss_file = 'styles/light_theme.qss' # Ensure your existing QSS file is named 'light_theme.qss'
try:
with open(qss_file, 'r') as f:
stylesheet = f.read()
QApplication.instance().setStyleSheet(stylesheet)
except Exception as e:
logger.error(f"Error loading stylesheet {qss_file}: {e}")
def show_api_key_dial
gitextract_yykb5mfu/
├── .gitignore
├── LICENSE
├── README.md
├── install_macos_linux_WSL.sh
├── main.py
├── modules/
│ ├── about.py
│ ├── converter.py
│ ├── exif_tab.py
│ ├── file_carving.py
│ ├── hex_tab.py
│ ├── mainwindow.py
│ ├── metadata_tab.py
│ ├── registry.py
│ ├── text_tab.py
│ ├── unified_application_manager.py
│ ├── verification.py
│ ├── veriphone_api.py
│ └── virus_total_tab.py
├── requirements.txt
├── requirements_macos_silicon.txt
├── styles/
│ ├── dark_theme.qss
│ └── light_theme.qss
└── tools/
├── Arsenal-Image-Mounter-v3.10.257/
│ ├── Arsenal Recon - End User License Agreement.txt
│ ├── ArsenalImageMounter.deps.json
│ ├── ArsenalImageMounter.log
│ ├── ArsenalImageMounter.runtimeconfig.json
│ ├── aim_cli.runtimeconfig.json
│ ├── readme.txt
│ └── readme_cli.txt
└── sleuthkit-4.12.1-win32/
├── NEWS.txt
├── README-win32.txt
├── README.txt
├── bin/
│ └── mactime.pl
├── lib/
│ ├── libtsk.lib
│ └── libtsk_jni.lib
└── licenses/
├── IBM-LICENSE
└── cpl1.0.txt
SYMBOL INDEX (469 symbols across 13 files)
FILE: modules/about.py
class AboutDialog (line 6) | class AboutDialog(QDialog):
method __init__ (line 7) | def __init__(self, parent=None):
FILE: modules/converter.py
function list_drives (line 18) | def list_drives():
class Main (line 34) | class Main(QMainWindow):
method __init__ (line 35) | def __init__(self):
method init_ui (line 47) | def init_ui(self):
method show_specific_widget (line 60) | def show_specific_widget(self, widget_name):
method show_select_source (line 73) | def show_select_source(self):
class DriveSelectionWidget (line 78) | class DriveSelectionWidget(QWidget):
method __init__ (line 82) | def __init__(self, parent=None):
method init_ui (line 86) | def init_ui(self):
method on_select_clicked (line 107) | def on_select_clicked(self):
class SelectSourceDialog (line 112) | class SelectSourceDialog(QWidget):
method __init__ (line 115) | def __init__(self, parent=None):
method on_next_clicked (line 145) | def on_next_clicked(self):
class ConversionWidget (line 156) | class ConversionWidget(QWidget):
method __init__ (line 159) | def __init__(self, parent=None):
method init_ui (line 166) | def init_ui(self):
method on_back_clicked (line 202) | def on_back_clicked(self):
method browse_file (line 206) | def browse_file(self):
method select_output_dir (line 211) | def select_output_dir(self):
method convert (line 216) | def convert(self):
method perform_conversion (line 238) | def perform_conversion(self, input_path, output_path):
FILE: modules/exif_tab.py
class ExifViewerManager (line 8) | class ExifViewerManager:
method __init__ (line 9) | def __init__(self):
method get_exif_data_from_content (line 13) | def get_exif_data_from_content(file_content):
method load_exif_data (line 29) | def load_exif_data(self, file_content):
class ExifViewer (line 49) | class ExifViewer(QWidget):
method __init__ (line 50) | def __init__(self, parent=None):
method init_ui (line 56) | def init_ui(self):
method display_exif_data (line 72) | def display_exif_data(self, exif_data):
method clear_content (line 115) | def clear_content(self):
method load_and_display_exif_data (line 119) | def load_and_display_exif_data(self, file_content):
FILE: modules/file_carving.py
class NumericTableWidgetItem (line 27) | class NumericTableWidgetItem(QTableWidgetItem):
method __lt__ (line 28) | def __lt__(self, other):
class FileCarvingWidget (line 42) | class FileCarvingWidget(QWidget):
method __init__ (line 45) | def __init__(self, parent=None):
method init_ui (line 55) | def init_ui(self):
method create_table_widget (line 117) | def create_table_widget(self):
method create_list_widget (line 169) | def create_list_widget(self):
method center_crop_to_square (line 219) | def center_crop_to_square(pixmap, target_size):
method render_svg_to_pixmap (line 245) | def render_svg_to_pixmap(svg_path, target_size):
method set_icon_size (line 263) | def set_icon_size(self, size):
method set_small_size (line 269) | def set_small_size(self):
method set_medium_size (line 272) | def set_medium_size(self):
method set_large_size (line 275) | def set_large_size(self):
method start_carving (line 278) | def start_carving(self):
method stop_carving (line 329) | def stop_carving(self):
method set_image_handler (line 334) | def set_image_handler(self, image_handler):
method is_offset_allocated (line 339) | def is_offset_allocated(offset, chunk_size, allocation_map):
method open_context_menu (line 368) | def open_context_menu(self, position):
method open_image (line 381) | def open_image(self):
method open_file_location (line 395) | def open_file_location(self):
method get_carved_timestamp (line 405) | def get_carved_timestamp(self, file_name):
method on_carved_file_clicked (line 412) | def on_carved_file_clicked(self, *args):
method setup_buttons (line 479) | def setup_buttons(self):
method start_carving_thread (line 485) | def start_carving_thread(self):
method stop_carving_thread (line 491) | def stop_carving_thread(self):
method is_valid_file (line 496) | def is_valid_file(self, data, file_type):
method carve_pdf_files (line 521) | def carve_pdf_files(self, chunk, global_offset):
method carve_wav_files (line 556) | def carve_wav_files(self, chunk, offset):
method carve_mov_files (line 581) | def carve_mov_files(self, chunk, offset):
method carve_jpg_files (line 631) | def carve_jpg_files(self, chunk, offset):
method carve_gif_files (line 652) | def carve_gif_files(self, chunk, offset):
method carve_png_files (line 673) | def carve_png_files(self, chunk, offset):
method carve_wmv_files (line 694) | def carve_wmv_files(self, chunk, offset):
method carve_zip_files (line 729) | def carve_zip_files(self, chunk, global_offset):
method carve_bmp_files (line 774) | def carve_bmp_files(self, chunk, offset):
method carve_files (line 817) | def carve_files(self, selected_file_types):
method extract_original_timestamp (line 885) | def extract_original_timestamp(file_content, file_type):
method save_file (line 936) | def save_file(self, file_content, file_type, file_path, offset):
method display_carved_file (line 968) | def display_carved_file(self, name, size, type_, modification_date, fi...
method clear (line 1053) | def clear(self):
method clear_ui (line 1060) | def clear_ui(self):
method handle_resize_event (line 1064) | def handle_resize_event(self, event):
FILE: modules/hex_tab.py
class SearchWorker (line 12) | class SearchWorker(QObject):
method __init__ (line 15) | def __init__(self, hex_viewer_manager, query):
method run (line 20) | def run(self):
class HexViewerManager (line 25) | class HexViewerManager:
method __init__ (line 28) | def __init__(self, hex_content, byte_content):
method format_hex (line 36) | def format_hex(self, page=0):
method format_hex_chunk (line 47) | def format_hex_chunk(self, start):
method total_pages (line 63) | def total_pages(self):
method search (line 66) | def search(self, query):
method search_by_address (line 79) | def search_by_address(self, address):
method search_by_string (line 91) | def search_by_string(self, query):
method search_by_hex (line 107) | def search_by_hex(self, hex_query):
class HexViewer (line 129) | class HexViewer(QWidget):
method __init__ (line 130) | def __init__(self, parent=None):
method show_context_menu (line 146) | def show_context_menu(self, pos):
method copy_to_clipboard (line 150) | def copy_to_clipboard(self):
method initialize_ui (line 176) | def initialize_ui(self):
method resizeEvent (line 233) | def resizeEvent(self, event: QResizeEvent):
method setup_toolbar (line 244) | def setup_toolbar(self):
method update_font_size (line 351) | def update_font_size(self):
method setup_hex_table (line 393) | def setup_hex_table(self):
method display_hex_content (line 445) | def display_hex_content(self, file_content):
method export_content (line 458) | def export_content(self, selected_format):
method export_as_text (line 479) | def export_as_text(self, file_name):
method export_as_html (line 492) | def export_as_html(self, file_name):
method parse_hex_line (line 517) | def parse_hex_line(self, line):
method clear_content (line 524) | def clear_content(self):
method load_first_page (line 527) | def load_first_page(self):
method load_last_page (line 534) | def load_last_page(self):
method next_page (line 541) | def next_page(self):
method previous_page (line 549) | def previous_page(self):
method search_result_clicked (line 557) | def search_result_clicked(self, item):
method display_current_page (line 561) | def display_current_page(self):
method go_to_page_by_entry (line 612) | def go_to_page_by_entry(self):
method update_navigation_states (line 624) | def update_navigation_states(self):
method update_total_pages_label (line 635) | def update_total_pages_label(self):
method trigger_search (line 640) | def trigger_search(self):
method cleanup_thread_resources (line 664) | def cleanup_thread_resources(self):
method closeEvent (line 673) | def closeEvent(self, event):
method handle_search_results (line 679) | def handle_search_results(self, matches):
method navigate_to_address (line 695) | def navigate_to_address(self, address):
FILE: modules/mainwindow.py
function safe_datetime (line 104) | def safe_datetime(timestamp):
class FileSystemUtils (line 114) | class FileSystemUtils:
method get_readable_size (line 116) | def get_readable_size(size_in_bytes):
method temp_file (line 129) | def temp_file():
class EWFImgInfo (line 142) | class EWFImgInfo(pytsk3.Img_Info):
method __init__ (line 143) | def __init__(self, ewf_handle):
method close (line 147) | def close(self):
method read (line 150) | def read(self, offset, size):
method get_size (line 154) | def get_size(self):
class ImageHandler (line 159) | class ImageHandler:
method __init__ (line 160) | def __init__(self, image_path):
method __del__ (line 173) | def __del__(self):
method close_resources (line 177) | def close_resources(self):
method get_size (line 200) | def get_size(self):
method read (line 207) | def read(self, offset, size):
method build_allocation_map (line 214) | def build_allocation_map(self, start_offset):
method get_image_type (line 313) | def get_image_type(self):
method calculate_hashes (line 330) | def calculate_hashes(self, progress_callback=None):
method load_image (line 425) | def load_image(self):
method has_filesystem (line 458) | def has_filesystem(self, start_offset):
method is_wiped (line 462) | def is_wiped(self):
method partitions (line 467) | def partitions(self):
method get_partitions (line 473) | def get_partitions(self):
method _get_partitions (line 477) | def _get_partitions(self):
method get_fs_info (line 488) | def get_fs_info(self, start_offset):
method get_fs_type (line 499) | def get_fs_type(self, start_offset):
method check_partition_contents (line 523) | def check_partition_contents(self, partition_start_offset):
method get_directory_contents (line 536) | def get_directory_contents(self, start_offset, inode_number=None):
method get_registry_hive (line 583) | def get_registry_hive(self, fs_info, hive_path):
method get_windows_version (line 593) | def get_windows_version(self, start_offset):
method read_unallocated_space (line 638) | def read_unallocated_space(self, start_offset, end_offset):
method open_image (line 673) | def open_image(self):
method list_files (line 682) | def list_files(self, extensions=None):
method process_partition (line 698) | def process_partition(self, img_info, offset_sectors, files_list, exte...
method _recursive_file_search (line 706) | def _recursive_file_search(self, fs_info, directory, parent_path, file...
method _get_directory_metadata (line 765) | def _get_directory_metadata(self, entry, parent_path, start_offset=0):
method _get_volume_name_for_offset (line 807) | def _get_volume_name_for_offset(self, start_offset):
method _get_file_metadata (line 820) | def _get_file_metadata(self, entry, parent_path, start_offset=0):
method search_files (line 863) | def search_files(self, search_query=None):
method process_partition_search (line 886) | def process_partition_search(self, img_info, offset_sectors, files_lis...
method get_file_content (line 898) | def get_file_content(self, inode_number, offset):
method get_readable_size (line 932) | def get_readable_size(self, size_in_bytes):
class DatabaseManager (line 938) | class DatabaseManager:
method __init__ (line 939) | def __init__(self, db_path):
method _connect (line 945) | def _connect(self):
method __del__ (line 955) | def __del__(self):
method close (line 959) | def close(self):
method get_icon_path (line 968) | def get_icon_path(self, icon_type, identifier):
class ImageManager (line 1020) | class ImageManager(QThread):
method __init__ (line 1025) | def __init__(self):
method __del__ (line 1033) | def __del__(self):
method cleanup_resources (line 1036) | def cleanup_resources(self):
method run (line 1045) | def run(self):
method _mount_image_windows (line 1073) | def _mount_image_windows(self):
method _mount_image_macos (line 1113) | def _mount_image_macos(self):
method _mount_image_linux (line 1198) | def _mount_image_linux(self):
method _dismount_image_linux (line 1267) | def _dismount_image_linux(self):
method _dismount_image_macos (line 1290) | def _dismount_image_macos(self):
method _dismount_image_windows (line 1364) | def _dismount_image_windows(self):
method dismount_image (line 1389) | def dismount_image(self):
method mount_image (line 1398) | def mount_image(self):
class SizeTableWidgetItem (line 1440) | class SizeTableWidgetItem(QTableWidgetItem):
method __lt__ (line 1442) | def __lt__(self, other):
class MainWindow (line 1448) | class MainWindow(QMainWindow):
method __init__ (line 1452) | def __init__(self):
method _handle_mount_operation_complete (line 1504) | def _handle_mount_operation_complete(self, success: bool, message: str...
method _get_file_icon (line 1512) | def _get_file_icon(self, file_extension: str) -> QIcon:
method _format_partition_text (line 1519) | def _format_partition_text(self, addr: int, desc: bytes, start: int, e...
method _confirm_exit (line 1526) | def _confirm_exit(self) -> bool:
method _handle_dismount_if_needed (line 1536) | def _handle_dismount_if_needed(self) -> None:
method _create_tree_item_for_entry (line 1552) | def _create_tree_item_for_entry(self, parent_item: QTreeWidgetItem, en...
method _setup_directory_tree_item (line 1565) | def _setup_directory_tree_item(self, item: QTreeWidgetItem, entry: Dic...
method _setup_file_tree_item (line 1588) | def _setup_file_tree_item(self, item: QTreeWidgetItem, entry: Dict[str...
method _populate_table_entry (line 1604) | def _populate_table_entry(self, row_position: int, entry: Dict[str, An...
method initialize_ui (line 1631) | def initialize_ui(self):
method apply_stylesheet (line 1967) | def apply_stylesheet(self, theme='light'):
method show_api_key_dialog (line 1980) | def show_api_key_dialog(self):
method save_api_keys (line 2016) | def save_api_keys(self, virus_total_key, veriphone_key, dialog):
method show_conversion_widget (line 2036) | def show_conversion_widget(self):
method show_veriphone_widget (line 2041) | def show_veriphone_widget(self):
method verify_image (line 2050) | def verify_image(self):
method on_verification_closed (line 2064) | def on_verification_closed(self, event):
method enable_tabs (line 2076) | def enable_tabs(self, state):
method create_menu (line 2083) | def create_menu(self, menu_bar, menu_name, actions):
method create_tree_item (line 2095) | def create_tree_item(parent, text, icon_path, data):
method on_viewer_dock_focus (line 2102) | def on_viewer_dock_focus(self, visible):
method clear_ui (line 2110) | def clear_ui(self):
method clear_viewers (line 2131) | def clear_viewers(self):
method closeEvent (line 2139) | def closeEvent(self, event):
method cleanup_resources (line 2151) | def cleanup_resources(self):
method load_image_evidence (line 2214) | def load_image_evidence(self):
method remove_image_evidence (line 2282) | def remove_image_evidence(self):
method remove_from_tree_viewer (line 2314) | def remove_from_tree_viewer(self, evidence_name):
method load_partitions_into_tree (line 2322) | def load_partitions_into_tree(self, image_path):
method populate_contents (line 2376) | def populate_contents(self, item: QTreeWidgetItem, data: Dict[str, Any...
method on_item_expanded (line 2386) | def on_item_expanded(self, item):
class FileContentWorker (line 2400) | class FileContentWorker(QThread):
method __init__ (line 2405) | def __init__(self, image_handler, inode_number, offset):
method run (line 2411) | def run(self):
class MediaStreamWorker (line 2422) | class MediaStreamWorker(QThread):
method __init__ (line 2426) | def __init__(self, image_handler, inode_number, offset):
method run (line 2432) | def run(self):
class UnallocatedSpaceWorker (line 2460) | class UnallocatedSpaceWorker(QThread):
method __init__ (line 2464) | def __init__(self, image_handler, start_offset, end_offset):
method run (line 2470) | def run(self):
method on_item_clicked (line 2480) | def on_item_clicked(self, item, column):
method update_directory_up_button (line 2586) | def update_directory_up_button(self):
method find_parent_inode (line 2612) | def find_parent_inode(self, start_offset, inode_number):
method navigate_up_directory (line 2634) | def navigate_up_directory(self):
method _add_to_history (line 2701) | def _add_to_history(self, directory_data):
method _update_navigation_buttons (line 2732) | def _update_navigation_buttons(self):
method navigate_back (line 2742) | def navigate_back(self):
method navigate_forward (line 2763) | def navigate_forward(self):
method _navigate_to_history_entry (line 2784) | def _navigate_to_history_entry(self, history_entry):
method select_tree_item_by_inode (line 2823) | def select_tree_item_by_inode(self, inode_number, start_offset):
method find_tree_item_recursive (line 2849) | def find_tree_item_recursive(self, parent_item, inode_number, start_of...
method display_volumes_in_listing (line 2872) | def display_volumes_in_listing(self) -> None:
method populate_listing_table (line 2995) | def populate_listing_table(self, entries: List[Dict[str, Any]], offset...
method insert_row_into_listing_table (line 3047) | def insert_row_into_listing_table(self, entry_name, entry_inode, descr...
method update_viewer_with_file_content (line 3091) | def update_viewer_with_file_content(self, file_content, data):
method update_viewer_with_media_stream (line 3128) | def update_viewer_with_media_stream(self, file_obj, file_size, metadat...
method display_content_for_active_tab (line 3159) | def display_content_for_active_tab(self):
method open_listing_context_menu (line 3229) | def open_listing_context_menu(self, position):
method handle_export (line 3258) | def handle_export(self, data, dest_dir):
method log_error (line 3299) | def log_error(self, message):
method open_tree_context_menu (line 3304) | def open_tree_context_menu(self, position):
method view_os_information (line 3325) | def view_os_information(self, index):
method _populate_volume_table (line 3551) | def _populate_volume_table(self, table, partitions):
method _extract_comprehensive_volume_info (line 3630) | def _extract_comprehensive_volume_info(self, start_offset):
method _get_image_info (line 3674) | def _get_image_info(self):
method _get_partition_info (line 3728) | def _get_partition_info(self, partition):
method _get_filesystem_colors (line 3798) | def _get_filesystem_colors(self):
method _create_space_allocation_chart (line 3816) | def _create_space_allocation_chart(self):
method create_action (line 3916) | def create_action(self, icon_path, text, callback):
method get_grandparent_inode (line 3921) | def get_grandparent_inode(self, parent_inode, start_offset):
method on_listing_table_item_clicked (line 3946) | def on_listing_table_item_clicked(self, item):
method navigate_tree_to_path (line 3970) | def navigate_tree_to_path(self, path, file_data):
method on_listing_search_text_changed (line 4052) | def on_listing_search_text_changed(self):
method trigger_listing_search (line 4060) | def trigger_listing_search(self):
method _execute_search (line 4079) | def _execute_search(self):
method clear_listing_search (line 4085) | def clear_listing_search(self):
method switch_to_search_mode (line 4092) | def switch_to_search_mode(self):
method switch_to_browse_mode (line 4120) | def switch_to_browse_mode(self):
method _restore_tree_selection (line 4157) | def _restore_tree_selection(self, path, directory_data):
method _wildcard_to_regex (line 4225) | def _wildcard_to_regex(self, pattern):
method _matches_wildcard (line 4234) | def _matches_wildcard(self, filename, pattern):
method perform_search (line 4239) | def perform_search(self, search_query):
method insert_search_result_row (line 4286) | def insert_search_result_row(self, file_data):
method apply_browse_filter (line 4333) | def apply_browse_filter(self, extensions):
method open_search_result_file (line 4368) | def open_search_result_file(self, file_data):
method show_file_in_directory (line 4374) | def show_file_in_directory(self, file_data):
method on_listing_table_item_clicked (line 4432) | def on_listing_table_item_clicked(self, item):
class ExportWorker (line 4518) | class ExportWorker(QThread):
method __init__ (line 4524) | def __init__(self, image_handler, inode_number, offset, dest_dir, name...
method run (line 4535) | def run(self):
method _export_directory (line 4546) | def _export_directory(self, inode_number, offset, dest_dir, name):
method _count_items_recursive (line 4575) | def _count_items_recursive(self, entries, offset):
method _export_item (line 4585) | def _export_item(self, inode_number, offset, dest_dir, name, is_direct...
method _export_file (line 4612) | def _export_file(self, inode_number, offset, dest_dir, name):
FILE: modules/metadata_tab.py
class MetadataViewer (line 9) | class MetadataViewer(QWidget):
method __init__ (line 10) | def __init__(self, image_handler):
method init_ui (line 15) | def init_ui(self):
method display_metadata (line 27) | def display_metadata(self, data):
method run_istat (line 127) | def run_istat(self, offset, inode_number, image_path):
method clear (line 159) | def clear(self):
FILE: modules/registry.py
class RegistryExtractor (line 13) | class RegistryExtractor(QWidget):
method __init__ (line 14) | def __init__(self, image_handler):
method init_ui (line 22) | def init_ui(self):
method onCustomContextMenuRequested (line 92) | def onCustomContextMenuRequested(self, position):
method load_selected_hive (line 107) | def load_selected_hive(self):
method display_registry_hive (line 141) | def display_registry_hive(self, hive_name, root_key):
method display_registry_keys (line 148) | def display_registry_keys(self, parent_item, registry_key):
method display_registry_values (line 157) | def display_registry_values(self, parent_key_item, registry_key):
method display_metadata (line 165) | def display_metadata(self, registry_object):
method setup_table (line 184) | def setup_table(self, values):
method display_values_in_table (line 208) | def display_values_in_table(self, values):
method on_item_clicked (line 211) | def on_item_clicked(self, item, column):
method clear (line 222) | def clear(self):
FILE: modules/text_tab.py
class SearchDirection (line 17) | class SearchDirection(Enum):
class TextViewerManager (line 22) | class TextViewerManager:
method __init__ (line 25) | def __init__(self):
method get_total_pages (line 36) | def get_total_pages(self):
method detect_encoding (line 40) | def detect_encoding(file_content_chunk):
method extract_strings_from_content (line 45) | def extract_strings_from_content(self):
method load_text_content (line 58) | def load_text_content(self, file_content):
method get_text_content_for_current_page (line 63) | def get_text_content_for_current_page(self):
method change_page (line 68) | def change_page(self, delta):
method jump_to_start (line 75) | def jump_to_start(self):
method jump_to_end (line 80) | def jump_to_end(self):
method search_for_string (line 85) | def search_for_string(self, search_str, direction=SearchDirection.NEXT):
method clear_content (line 119) | def clear_content(self):
class TextViewer (line 127) | class TextViewer(QWidget):
method __init__ (line 128) | def __init__(self, parent=None):
method init_ui (line 135) | def init_ui(self):
method setup_toolbar (line 145) | def setup_toolbar(self):
method setup_text_edit (line 227) | def setup_text_edit(self):
method display_text_content (line 232) | def display_text_content(self, file_content):
method clear_content (line 236) | def clear_content(self):
method search_next (line 240) | def search_next(self):
method update_highlighted_text (line 247) | def update_highlighted_text(self):
method update_font_size (line 264) | def update_font_size(self):
method go_to_page_by_entry (line 270) | def go_to_page_by_entry(self):
method refresh_content (line 281) | def refresh_content(self):
class CustomTextEdit (line 290) | class CustomTextEdit(QTextEdit):
method __init__ (line 291) | def __init__(self, *args, **kwargs):
method contextMenuEvent (line 295) | def contextMenuEvent(self, event):
method decodeBase64 (line 315) | def decodeBase64(self):
method decodeHex (line 318) | def decodeHex(self):
method decodeURL (line 321) | def decodeURL(self):
method decodeHTML (line 324) | def decodeHTML(self):
method decodeOctal (line 327) | def decodeOctal(self):
method decodeBinary (line 330) | def decodeBinary(self):
method decodeSelectedText (line 333) | def decodeSelectedText(self, encoding_type):
method getDecodedText (line 366) | def getDecodedText(self, selected_text):
method tryDecodeBase64 (line 382) | def tryDecodeBase64(self, text):
method tryDecodeHex (line 389) | def tryDecodeHex(self, text):
method tryDecodeURL (line 396) | def tryDecodeURL(self, text):
method tryDecodeHTML (line 402) | def tryDecodeHTML(self, text):
method tryDecodeOctal (line 408) | def tryDecodeOctal(self, text):
method tryDecodeBinary (line 414) | def tryDecodeBinary(self, text):
method mouseMoveEvent (line 422) | def mouseMoveEvent(self, event):
FILE: modules/unified_application_manager.py
class PyTsk3StreamDevice (line 24) | class PyTsk3StreamDevice(QIODevice):
method __init__ (line 27) | def __init__(self, file_obj, file_size, parent=None):
method size (line 35) | def size(self):
method isSequential (line 39) | def isSequential(self):
method seek (line 43) | def seek(self, pos):
method pos (line 51) | def pos(self):
method atEnd (line 55) | def atEnd(self):
method readData (line 59) | def readData(self, maxSize):
method writeData (line 89) | def writeData(self, data):
method close (line 93) | def close(self):
class UnifiedViewer (line 100) | class UnifiedViewer(QWidget):
method __init__ (line 101) | def __init__(self, parent=None):
method ensure_icons_directory (line 129) | def ensure_icons_directory(self):
method create_default_icon (line 146) | def create_default_icon(self, path, size, color):
method get_pdf_viewer (line 191) | def get_pdf_viewer(self):
method get_picture_viewer (line 199) | def get_picture_viewer(self):
method get_audio_video_player (line 207) | def get_audio_video_player(self):
method load (line 215) | def load(self, content=None, mime_type=None, path=None, file_obj=None,...
method clear (line 326) | def clear(self):
method display_application_content (line 385) | def display_application_content(self, file_content, full_file_path):
method closeEvent (line 406) | def closeEvent(self, event):
method __del__ (line 439) | def __del__(self):
method shutdown (line 451) | def shutdown(self):
class PictureViewer (line 503) | class PictureViewer(QWidget):
method __init__ (line 504) | def __init__(self, parent=None):
method initialize_ui (line 510) | def initialize_ui(self):
method setup_toolbar (line 543) | def setup_toolbar(self):
method display (line 581) | def display(self, content):
method clear (line 589) | def clear(self):
method zoom_in (line 592) | def zoom_in(self):
method zoom_out (line 597) | def zoom_out(self):
method rotate_left (line 602) | def rotate_left(self):
method rotate_right (line 607) | def rotate_right(self):
method reset (line 612) | def reset(self):
method export_original_image (line 616) | def export_original_image(self):
class PDFViewer (line 633) | class PDFViewer(QWidget):
method __init__ (line 634) | def __init__(self, parent=None):
method initialize_ui (line 651) | def initialize_ui(self):
method setup_toolbar (line 680) | def setup_toolbar(self):
method setup_pdf_display_area (line 793) | def setup_pdf_display_area(self):
method set_current_page (line 803) | def set_current_page(self, page_num):
method go_to_page (line 813) | def go_to_page(self):
method update_navigation_states (line 821) | def update_navigation_states(self):
method show_previous_page (line 839) | def show_previous_page(self):
method show_next_page (line 844) | def show_next_page(self):
method show_page (line 849) | def show_page(self, page_num):
method display (line 884) | def display(self, content):
method cleanup_pdf_content (line 916) | def cleanup_pdf_content(content):
method clear (line 958) | def clear(self):
method show_first_page (line 969) | def show_first_page(self):
method show_last_page (line 973) | def show_last_page(self):
method zoom_in (line 978) | def zoom_in(self):
method zoom_out (line 989) | def zoom_out(self):
method set_zoom_from_entry (line 1000) | def set_zoom_from_entry(self):
method reset_zoom (line 1021) | def reset_zoom(self):
method fit_window (line 1029) | def fit_window(self):
method fit_width (line 1044) | def fit_width(self):
method rotate_left (line 1057) | def rotate_left(self):
method rotate_right (line 1064) | def rotate_right(self):
method toggle_pan_mode (line 1071) | def toggle_pan_mode(self, checked):
method mousePressEvent (line 1076) | def mousePressEvent(self, event):
method mouseMoveEvent (line 1085) | def mouseMoveEvent(self, event):
method mouseReleaseEvent (line 1101) | def mouseReleaseEvent(self, event):
method print_pdf (line 1108) | def print_pdf(self):
method save_pdf (line 1155) | def save_pdf(self):
class AudioVideoPlayer (line 1178) | class AudioVideoPlayer(QWidget):
method __init__ (line 1179) | def __init__(self, parent=None):
method initialize_ui (line 1198) | def initialize_ui(self):
method set_audio_only_mode (line 1231) | def set_audio_only_mode(self, is_audio_only=True):
method handle_media_status_change (line 1251) | def handle_media_status_change(self, status):
method setup_connections (line 1267) | def setup_connections(self):
method _setup_os_volume (line 1285) | def _setup_os_volume(self):
method set_os_volume (line 1299) | def set_os_volume(self, volume_level):
method toggle_play (line 1308) | def toggle_play(self):
method stop (line 1314) | def stop(self):
method update_play_state (line 1324) | def update_play_state(self, state):
method update_controls (line 1329) | def update_controls(self):
method set_position (line 1363) | def set_position(self, position):
method update_position (line 1366) | def update_position(self, position):
method update_duration (line 1375) | def update_duration(self, duration):
method format_time (line 1379) | def format_time(self, milliseconds):
method toggle_mute (line 1385) | def toggle_mute(self):
method set_volume (line 1425) | def set_volume(self, volume):
method handle_error (line 1472) | def handle_error(self, error, error_string):
method closeEvent (line 1476) | def closeEvent(self, event):
method __del__ (line 1485) | def __del__(self):
method create_controls (line 1494) | def create_controls(self):
method safe_stop (line 1594) | def safe_stop(self):
FILE: modules/verification.py
class HashCalculationThread (line 7) | class HashCalculationThread(QThread):
method __init__ (line 11) | def __init__(self, image_handler):
method run (line 16) | def run(self):
method update_progress (line 29) | def update_progress(self, current, total):
method stop (line 39) | def stop(self):
class VerificationWidget (line 44) | class VerificationWidget(QWidget):
method __init__ (line 45) | def __init__(self, image_handler, parent=None):
method closeEvent (line 127) | def closeEvent(self, event):
method save_hash (line 140) | def save_hash(self):
method start_hash_calculation (line 146) | def start_hash_calculation(self):
method update_progress (line 157) | def update_progress(self, percentage):
method on_hash_calculated (line 165) | def on_hash_calculated(self, hash_results):
method copy_hash (line 222) | def copy_hash(self):
method is_verified (line 227) | def is_verified(self):
FILE: modules/veriphone_api.py
class VeriphoneWidget (line 8) | class VeriphoneWidget(QWidget):
method __init__ (line 9) | def __init__(self):
method init_ui (line 14) | def init_ui(self):
method set_api_key (line 70) | def set_api_key(self, key):
method use_api_key (line 73) | def use_api_key(self):
method verify_phone_number (line 77) | def verify_phone_number(self):
method update_veriphone_info (line 89) | def update_veriphone_info(self, phone_number):
method verify_phone_with_veriphone (line 97) | def verify_phone_with_veriphone(self, phone_number):
method format_data_as_html (line 105) | def format_data_as_html(self, data):
FILE: modules/virus_total_tab.py
class VirusTotal (line 15) | class VirusTotal(QWidget):
method __init__ (line 16) | def __init__(self):
method init_ui (line 29) | def init_ui(self):
method set_api_key (line 70) | def set_api_key(self, key):
method use_api_key (line 73) | def use_api_key(self):
method spacer (line 77) | def spacer(self, policy1, policy2):
method setup_logo_toolbar (line 82) | def setup_logo_toolbar(self):
method setup_action_toolbar (line 92) | def setup_action_toolbar(self):
method virus_total_website (line 112) | def virus_total_website(self, event):
method reset_ui (line 116) | def reset_ui(self):
method set_file_hash (line 122) | def set_file_hash(self, file_hash):
method set_file_content (line 126) | def set_file_content(self, file_content, file_name="unnamed_file"):
method upload_file (line 134) | def upload_file(self):
method zip_file_in_memory (line 149) | def zip_file_in_memory(self, content: bytes, file_name: str):
method upload_file_to_virustotal (line 157) | def upload_file_to_virustotal(self, file_content, file_name):
method process_vt_response (line 175) | def process_vt_response(self, response):
method pass_hash (line 186) | def pass_hash(self):
method update_virustotal_info (line 201) | def update_virustotal_info(self):
method vt_getresult (line 213) | def vt_getresult(self, hashes):
method format_data_as_html (line 261) | def format_data_as_html(self, data):
method view_in_browser (line 319) | def view_in_browser(self):
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (832K chars).
[
{
"path": ".gitignore",
"chars": 361,
"preview": "# Ignore E01, dd, and raw image files in the root directory\n/*.E01\n/*.dd\n/*.raw\n\n# Ignore IDE-specific folders and files"
},
{
"path": "LICENSE",
"chars": 1076,
"preview": "MIT License\n\nCopyright (c) 2024 Radoslav Gadzhovski\n\nPermission is hereby granted, free of charge, to any person obtaini"
},
{
"path": "README.md",
"chars": 9430,
"preview": "<h1 align=\"center\">Toolkit for Retrieval and Analysis of Cyber Evidence (TRACE)</h1>\n\n<p align=\"center\">\n TRACE is a di"
},
{
"path": "install_macos_linux_WSL.sh",
"chars": 7158,
"preview": "#!/bin/bash\nset -e\n\n# === COLORS ===\nGREEN=\"\\033[1;32m\"\nLIGHT_GREEN=\"\\033[38;5;82m\"\nCYAN=\"\\033[1;36m\"\nMAGENTA=\"\\033[1;35"
},
{
"path": "main.py",
"chars": 204,
"preview": "\nfrom PySide6.QtWidgets import QApplication\nfrom modules.mainwindow import MainWindow\n\n\nif __name__ == '__main__':\n a"
},
{
"path": "modules/about.py",
"chars": 2129,
"preview": "from PySide6.QtCore import Qt\nfrom PySide6.QtGui import QPixmap, QFont, QPalette, QColor\nfrom PySide6.QtWidgets import Q"
},
{
"path": "modules/converter.py",
"chars": 9468,
"preview": "import os\nimport subprocess\n\nimport pyewf\nfrom PySide6.QtCore import Signal\nfrom PySide6.QtGui import QIcon\nfrom PySide6"
},
{
"path": "modules/exif_tab.py",
"chars": 4389,
"preview": "from io import BytesIO as io_BytesIO\n\nfrom PIL import Image\nfrom PIL.ExifTags import TAGS\nfrom PySide6.QtWidgets import "
},
{
"path": "modules/file_carving.py",
"chars": 47697,
"preview": "import datetime\nimport io\nimport os\nimport struct\nimport time\nimport zipfile\nfrom concurrent.futures import ThreadPoolEx"
},
{
"path": "modules/hex_tab.py",
"chars": 30204,
"preview": "import os\nfrom functools import lru_cache\n\nfrom PySide6.QtCore import Qt, QObject, Signal, QThread, QSize\nfrom PySide6.Q"
},
{
"path": "modules/mainwindow.py",
"chars": 201137,
"preview": "import configparser\nimport hashlib\nimport os\nimport datetime\nimport pyewf\nimport pytsk3\nimport tempfile\nimport gc\nimport"
},
{
"path": "modules/metadata_tab.py",
"chars": 7618,
"preview": "import os\nimport datetime\nfrom PySide6.QtWidgets import QTextEdit, QSizePolicy, QWidget, QVBoxLayout\nimport hashlib\nfrom"
},
{
"path": "modules/registry.py",
"chars": 9839,
"preview": "import os\nimport tempfile\n\nfrom PySide6.QtCore import Qt\nfrom PySide6.QtGui import QIcon\nfrom PySide6.QtWidgets import Q"
},
{
"path": "modules/text_tab.py",
"chars": 15979,
"preview": "import base64\nimport html\nimport re\nimport sqlite3\nimport urllib\nimport urllib.parse\nfrom enum import Enum\nfrom functool"
},
{
"path": "modules/unified_application_manager.py",
"chars": 64728,
"preview": "import os\nfrom ctypes import cast, POINTER\nfrom weakref import WeakValueDictionary\nimport mimetypes\nimport platform\nimpo"
},
{
"path": "modules/verification.py",
"chars": 9735,
"preview": "from PySide6.QtGui import QIcon, QFont\nfrom PySide6.QtWidgets import (QWidget, QLabel, QVBoxLayout, QPushButton, QApplic"
},
{
"path": "modules/veriphone_api.py",
"chars": 5427,
"preview": "import requests\nfrom PySide6.QtCore import Qt\nfrom PySide6.QtGui import QPixmap, QIcon\nfrom PySide6.QtWidgets import (QW"
},
{
"path": "modules/virus_total_tab.py",
"chars": 13518,
"preview": "import io\nimport zipfile\nfrom datetime import date\nfrom time import time\n\nfrom PySide6.QtCore import Qt\nfrom PySide6.QtG"
},
{
"path": "requirements.txt",
"chars": 1324,
"preview": "aiohttp==3.8.5\naiosignal==1.3.1\nasync-timeout==4.0.3\nattrs==23.1.0\nav==16.0.0\ncertifi==2023.7.22\nchardet==5.2.0\ncharset"
},
{
"path": "requirements_macos_silicon.txt",
"chars": 1257,
"preview": "aiohttp==3.8.5\naiosignal==1.3.1\nasync-timeout==4.0.3\nattrs==23.1.0\nav==16.0.0\ncertifi==2023.7.22\ncharset-normalizer==3.3"
},
{
"path": "styles/dark_theme.qss",
"chars": 15123,
"preview": "/* Global Widget Styles */\nQWidget {\n font-size: 14px;\n color: #E0E0E0; /* Light text color */\n background-col"
},
{
"path": "styles/light_theme.qss",
"chars": 14193,
"preview": "/* Global Widget Styles */\nQWidget {\n font-size: 14px;\n color: #333333;\n background-color: #FFFFFF;\n}\n\n/* Butto"
},
{
"path": "tools/Arsenal-Image-Mounter-v3.10.257/Arsenal Recon - End User License Agreement.txt",
"chars": 27814,
"preview": "ARSENAL RECON END USER LICENSE AGREEMENT\n\nPLEASE READ THIS LICENSE AGREEMENT CAREFULLY BEFORE USING THIS SOFTWARE. BY IN"
},
{
"path": "tools/Arsenal-Image-Mounter-v3.10.257/ArsenalImageMounter.deps.json",
"chars": 72195,
"preview": "{\n \"runtimeTarget\": {\n \"name\": \".NETCoreApp,Version=v6.0\",\n \"signature\": \"\"\n },\n \"compilationOptions\": {},\n \"t"
},
{
"path": "tools/Arsenal-Image-Mounter-v3.10.257/ArsenalImageMounter.log",
"chars": 759,
"preview": "---------------\n2023-09-06 14:42:21\nStarting up: Application 'C:\\Users\\Radi\\Desktop\\Final_year_project\\Arsenal-Image-Mou"
},
{
"path": "tools/Arsenal-Image-Mounter-v3.10.257/ArsenalImageMounter.runtimeconfig.json",
"chars": 355,
"preview": "{\n \"runtimeOptions\": {\n \"tfm\": \"net6.0\",\n \"frameworks\": [\n {\n \"name\": \"Microsoft.NETCore.App\",\n "
},
{
"path": "tools/Arsenal-Image-Mounter-v3.10.257/aim_cli.runtimeconfig.json",
"chars": 242,
"preview": "{\n \"runtimeOptions\": {\n \"tfm\": \"net6.0\",\n \"framework\": {\n \"name\": \"Microsoft.NETCore.App\",\n \"version\": "
},
{
"path": "tools/Arsenal-Image-Mounter-v3.10.257/readme.txt",
"chars": 66037,
"preview": "Please read \"Arsenal Recon - End User License Agreement.txt\" carefully before using this software.\n\nArsenal Image Mounte"
},
{
"path": "tools/Arsenal-Image-Mounter-v3.10.257/readme_cli.txt",
"chars": 7297,
"preview": "Please read \"Arsenal Recon - End User License Agreement.txt\" carefully before using this software.\n\nArsenal Image Mounte"
},
{
"path": "tools/sleuthkit-4.12.1-win32/NEWS.txt",
"chars": 91509,
"preview": "---------------- VERSION 4.12.1 --------------\nC/C++:\n- Bug fixes from Luis Nassif and Joachim Metz\n- Added check to sto"
},
{
"path": "tools/sleuthkit-4.12.1-win32/README-win32.txt",
"chars": 2324,
"preview": " The Sleuth Kit\n Windows Executables\n\n http://www.sleuthk"
},
{
"path": "tools/sleuthkit-4.12.1-win32/README.txt",
"chars": 9085,
"preview": "[](https://travis-ci.org/sleuthkit/sleuthki"
},
{
"path": "tools/sleuthkit-4.12.1-win32/bin/mactime.pl",
"chars": 27249,
"preview": "my $VER=\"4.12.1\";\n#\n# This program is based on the 'mactime' program by Dan Farmer and\n# and the 'mac_daddy' program by "
},
{
"path": "tools/sleuthkit-4.12.1-win32/licenses/IBM-LICENSE",
"chars": 11954,
"preview": "IBM PUBLIC LICENSE VERSION 1.0 - CORONER TOOLKIT UTILITIES\n\nTHE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS"
},
{
"path": "tools/sleuthkit-4.12.1-win32/licenses/cpl1.0.txt",
"chars": 11613,
"preview": "Common Public License Version 1.0\n\nTHE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS COMMON PUBLIC\nLICENSE (\""
}
]
// ... and 2 more files (download for full content)
About this extraction
This page contains the full source code of the Gadzhovski/TRACE-Forensic-Toolkit GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (53.0 MB), approximately 189.8k tokens, and a symbol index with 469 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.