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
================================================
Toolkit for Retrieval and Analysis of Cyber Evidence (TRACE)
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.
## 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)
## 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.
## Screenshots 📸 [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)
### Registry Browser 🗂️
### File Carving 🔪
### File Search 🔍
### Image Verification ✅
## 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` | ✔️ | ✔️ |
## Tested File Systems 🗂️ [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)
| File System | Tested |
|-------------|--------|
| NTFS | ✔️ |
| FAT32 | |
| exFAT | |
| HFS+ | |
| APFS | |
| EXT2,3,4 | |
## Cross-Platform Compatibility 🍏🐧🗔 [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)
| Operating System | Screenshot |
|------------------------------------|----------------------------------------------------------------------------------------------------------------------|
| **macOS Sonoma** 🍏 |
|
| **Kali Linux 2024** 🐧 |
|
| **\*WSL2 - Ubuntu 22.04.3 LTS** 🐧 |
|
| **Windows 10** 🗔 |
|
## Getting Started 🚀 [⬆️](#toolkit-for-retrieval-and-analysis-of-cyber-evidence-trace)
### Installation ⚙️
#### **Windows:**
1. Install Python 3.11
(⚠️ Python 3.12 is not supported)
[👉 Download from python.org](https://www.python.org/downloads/release/python-3110/)
2. Install Microsoft C++ Build Tools
[👉 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)


================================================
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"""
"""
for key, value in exif_data:
exif_table += f"| {key} | {value} |
"
exif_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(" 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 = "\n"
html_content += "\n"
# Add a smaller and less prominent header with the original text
header_line = 'Generated by Trace
'
html_content += header_line + "
\n"
# Add directory and file name information
directory, filename = os.path.split(file_name)
html_content += f'Directory: {directory}
\n'
html_content += f'File Name: {filename}
\n'
# Add the green header line
header_line = ('Address 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F '
'ASCII')
html_content += header_line + "
\n"
html_content += self.hex_viewer_manager.format_hex(self.current_page).replace("\n", "
")
html_content += "\n"
html_content += ""
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_dialog(self):
# Create a dialog to get API keys from the user
dialog = QDialog(self)
dialog.setWindowTitle("API Key Configuration")
dialog.setFixedWidth(API_DIALOG_WIDTH) # Set a fixed width to accommodate longer API keys
# Set layout as a form layout for better presentation
layout = QFormLayout()
layout.setSpacing(10) # Add some spacing between fields
layout.setContentsMargins(15, 15, 15, 15) # Set content margins for better visual aesthetics
# VirusTotal API Key
virus_total_label = QLabel("VirusTotal API Key:")
virus_total_input = QLineEdit()
virus_total_input.setText(self.api_keys.get('API_KEYS', 'virustotal', fallback=''))
virus_total_input.setMinimumWidth(INPUT_FIELD_MIN_WIDTH) # Set a minimum width for the input field
layout.addRow(virus_total_label, virus_total_input)
# Veriphone API Key
veriphone_label = QLabel("Veriphone API Key:")
veriphone_input = QLineEdit()
veriphone_input.setText(self.api_keys.get('API_KEYS', 'veriphone', fallback=''))
veriphone_input.setMinimumWidth(INPUT_FIELD_MIN_WIDTH) # Set a minimum width for the input field
layout.addRow(veriphone_label, veriphone_input)
# Buttons
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(
lambda: self.save_api_keys(virus_total_input.text(), veriphone_input.text(), dialog))
button_box.rejected.connect(dialog.reject)
layout.addRow(button_box)
# Set layout and execute dialog
dialog.setLayout(layout)
dialog.exec_()
def save_api_keys(self, virus_total_key, veriphone_key, dialog):
# Save the API keys in a configuration file
if not self.api_keys.has_section('API_KEYS'):
self.api_keys.add_section('API_KEYS')
self.api_keys.set('API_KEYS', 'virustotal', virus_total_key)
self.api_keys.set('API_KEYS', 'veriphone', veriphone_key)
with open('config.ini', 'w') as config_file:
self.api_keys.write(config_file)
dialog.accept()
# Pass the updated API keys to the appropriate modules
self.virus_total_api.set_api_key(virus_total_key)
# Set Veriphone API key only if the widget is created
if hasattr(self, 'veriphone_widget'):
self.veriphone_widget.set_api_key(veriphone_key)
def show_conversion_widget(self):
"""Show the conversion widget."""
self.select_dialog = Main()
self.select_dialog.show()
def show_veriphone_widget(self):
"""Create the VeriphoneWidget only if it hasn't been created yet."""
if not hasattr(self, 'veriphone_widget'):
self.veriphone_widget = VeriphoneWidget()
# Set the API key after creating the widget
veriphone_key = self.api_keys.get('API_KEYS', 'veriphone', fallback='')
self.veriphone_widget.set_api_key(veriphone_key)
self.veriphone_widget.show()
def verify_image(self):
if self.image_handler is None:
QMessageBox.warning(self, "Verify Image", "No image is currently loaded.")
return
# Show the verification widget
self.verification_widget = VerificationWidget(self.image_handler)
# Connect a signal when the verification widget is closed to update the icon
self.verification_widget.closeEvent = lambda event: self.on_verification_closed(event)
# Show the widget
self.verification_widget.show()
def on_verification_closed(self, event):
"""Handle the verification widget being closed."""
# Make sure verify_image_button exists before trying to change its icon
if hasattr(self, 'verify_image_button'):
if hasattr(self.verification_widget, 'is_verified') and self.verification_widget.is_verified:
self.verify_image_button.setIcon(QIcon('Icons/icons8-verify-48_gren.png'))
else:
self.verify_image_button.setIcon(QIcon('Icons/icons8-verify-blue.png'))
# Call the original closeEvent to close the widget
QWidget.closeEvent(self.verification_widget, event)
def enable_tabs(self, state):
self.result_viewer.setEnabled(state)
self.viewer_tab.setEnabled(state)
self.listing_table.setEnabled(state)
self.deleted_files_widget.setEnabled(state)
self.registry_extractor_widget.setEnabled(state)
def create_menu(self, menu_bar, menu_name, actions):
menu = QMenu(menu_name, self)
for action_name, action_function in actions.items():
if action_name == 'separator':
menu.addSeparator()
else:
action = menu.addAction(action_name)
action.triggered.connect(action_function)
menu_bar.addMenu(menu)
return menu
@staticmethod
def create_tree_item(parent, text, icon_path, data):
item = QTreeWidgetItem(parent)
item.setText(0, text)
item.setIcon(0, QIcon(icon_path))
item.setData(0, Qt.UserRole, data)
return item
def on_viewer_dock_focus(self, visible):
if visible: # If the QDockWidget is focused/visible
self.viewer_dock.setMaximumSize(QT_MAX_SIZE, QT_MAX_SIZE) # Remove size constraints
else: # If the QDockWidget loses focus
current_height = self.viewer_dock.size().height() # Get the current height
self.viewer_dock.setMinimumSize(VIEWER_DOCK_MAX_WIDTH, current_height)
self.viewer_dock.setMaximumSize(VIEWER_DOCK_MAX_WIDTH, current_height)
def clear_ui(self):
self.listing_table.clearContents()
self.listing_table.setRowCount(0)
self.clear_viewers()
self.current_image_path = None
self.current_offset = None
self.image_mounted = False
self.evidence_files.clear()
self.deleted_files_widget.clear()
# Clear search bar and reset filters
self.listing_search_bar.clear()
# Clear navigation history
self._directory_history = []
self._history_index = -1
self._update_navigation_buttons()
# Disable directory up button
self.go_up_action.setEnabled(False)
def clear_viewers(self):
self.hex_viewer.clear_content()
self.text_viewer.clear_content()
self.application_viewer.clear()
self.metadata_viewer.clear()
self.exif_viewer.clear_content()
self.registry_extractor_widget.clear()
def closeEvent(self, event):
"""Handle application close event."""
if not self._confirm_exit():
event.ignore()
return
self._handle_dismount_if_needed()
# Cleanup resources
self.cleanup_resources()
event.accept()
def cleanup_resources(self):
"""Clean up all resources when closing the application."""
# Clean up application viewer first to ensure media players are properly shut down
try:
if hasattr(self, 'application_viewer'):
if hasattr(self.application_viewer, 'shutdown'):
self.application_viewer.shutdown()
else:
self.application_viewer.clear()
except Exception as e:
logger.error(f"Error shutting down application viewer: {e}")
# Stop any running background operations
for attr_name in dir(self):
attr = getattr(self, attr_name)
# Check if it's a thread and running
if isinstance(attr, QThread) and hasattr(attr, 'isRunning') and attr.isRunning():
try:
# Try to stop it gracefully
attr.quit()
attr.wait(1000) # Wait up to 1 second
# If still running, terminate it
if attr.isRunning():
attr.terminate()
except Exception as e:
logger.error(f"Error stopping thread {attr_name}: {str(e)}")
# Clean up image handler resources
if self.image_handler:
try:
self.image_handler.close_resources()
except Exception as e:
logger.error(f"Error closing image handler: {str(e)}")
# Close database connection
if hasattr(self, 'db_manager') and self.db_manager:
try:
self.db_manager.close()
except Exception as e:
logger.error(f"Error closing database connection: {str(e)}")
# Clean up temp files
temp_dir = tempfile.gettempdir()
pattern = "trace_temp_*"
try:
for item in os.listdir(temp_dir):
if item.startswith("trace_temp_"):
item_path = os.path.join(temp_dir, item)
try:
if os.path.isfile(item_path):
os.remove(item_path)
elif os.path.isdir(item_path):
import shutil
shutil.rmtree(item_path)
except Exception as e:
logger.error(f"Error removing temp file {item_path}: {str(e)}")
except Exception as e:
logger.error(f"Error cleaning up temp files: {str(e)}")
# Release any other resources
gc.collect() # Encourage garbage collection
def load_image_evidence(self):
"""Open an image with a specific filter on Kali Linux."""
# Define the supported image file extensions, including both lowercase and uppercase variants
supported_image_extensions = ["*.e01", "*.E01", "*.s01", "*.S01",
"*.l01", "*.L01", "*.raw", "*.RAW",
"*.img", "*.IMG", "*.dd", "*.DD",
"*.iso", "*.ISO", "*.ad1", "*.AD1",
"*.001", "*.s01", "*.ex01", "*.dmg",
"*.sparse", "*.sparseimage"]
# Construct the file filter string with both uppercase and lowercase extensions
file_filter = "Supported Image Files ({})".format(" ".join(supported_image_extensions))
# Open file dialog with the specified file filter
image_path, _ = QFileDialog.getOpenFileName(self, "Select Image", "", file_filter)
if image_path:
try:
image_path = os.path.normpath(image_path)
# Create a progress dialog to show loading status
progress = QProgressDialog("Loading image...", "Cancel", 0, 100, self)
progress.setWindowTitle("Loading Evidence")
progress.setWindowModality(Qt.WindowModal)
progress.setMinimumDuration(PROGRESS_MIN_DURATION) # Show dialog only if operation takes more than threshold
progress.setValue(10)
# Clean up any existing ImageHandler resources
if self.image_handler:
self.image_handler.close_resources()
# Create or update the ImageHandler instance with progress updates
progress.setValue(20)
# Process events to update UI
QApplication.processEvents()
# Create a new ImageHandler with the selected image
self.image_handler = ImageHandler(image_path)
progress.setValue(50)
# Add the image to evidence files list
if image_path not in self.evidence_files:
self.evidence_files.append(image_path)
self.current_image_path = image_path
progress.setValue(70)
# Pass the image handler to widgets that need it
self.deleted_files_widget.set_image_handler(self.image_handler)
self.registry_extractor_widget.image_handler = self.image_handler
self.metadata_viewer.image_handler = self.image_handler
progress.setValue(80)
# Load partitions into tree view
QApplication.processEvents()
self.load_partitions_into_tree(image_path)
progress.setValue(100)
# Enable all tabs since we have a valid image
self.enable_tabs(True)
except Exception as e:
QMessageBox.critical(self, "Error Loading Image", f"Failed to load image: {str(e)}")
# Remove the image from evidence files if it was added but failed to load
if image_path in self.evidence_files:
self.evidence_files.remove(image_path)
def remove_image_evidence(self):
if not self.evidence_files:
QMessageBox.warning(self, "Remove Evidence", "No evidence is currently loaded.")
return
# Prepare the options for the dialog
options = self.evidence_files + ["Remove All"]
selected_option, ok = QInputDialog.getItem(self, "Remove Evidence File",
"Select an evidence file to remove or 'Remove All':",
options, 0, False)
if ok:
if selected_option == "Remove All":
# Remove all evidence files
self.tree_viewer.invisibleRootItem().takeChildren() # Remove all children from the tree viewer
self.clear_ui() # Clear the UI
QMessageBox.information(self, "Remove Evidence", "All evidence files have been removed.")
else:
# Remove the selected evidence file
self.evidence_files.remove(selected_option)
self.remove_from_tree_viewer(selected_option)
self.clear_ui()
QMessageBox.information(self, "Remove Evidence", f"{selected_option} has been removed.")
# clear all tabs if there are no evidence files loaded
if not self.evidence_files:
self.clear_ui()
# disable all tabs
self.enable_tabs(False)
# set the icon back to the original - only if verify_image_button exists
if hasattr(self, 'verify_image_button'):
self.verify_image_button.setIcon(QIcon('Icons/icons8-verify-blue.png'))
def remove_from_tree_viewer(self, evidence_name):
root = self.tree_viewer.invisibleRootItem()
for i in range(root.childCount()):
item = root.child(i)
if item.text(0) == evidence_name:
root.removeChild(item)
break
def load_partitions_into_tree(self, image_path):
"""Load partitions from an image into the tree viewer."""
root_item_tree = self.create_tree_item(self.tree_viewer, image_path,
self.db_manager.get_icon_path('device', 'media-optical'),
{"start_offset": 0})
partitions = self.image_handler.get_partitions()
# Check if the image has partitions or a recognizable file system
if not partitions:
if self.image_handler.has_filesystem(0):
# The image has a filesystem but no partitions, populate root directory
self.populate_contents(root_item_tree, {"start_offset": 0})
else:
# Entire image is considered as unallocated space
size_in_bytes = self.image_handler.get_size()
readable_size = self.image_handler.get_readable_size(size_in_bytes)
unallocated_item_text = f"Unallocated Space: Size: {readable_size}"
self.create_tree_item(root_item_tree, unallocated_item_text,
self.db_manager.get_icon_path('file', 'unknown'),
{"is_unallocated": True, "start_offset": 0,
"end_offset": size_in_bytes // SECTOR_SIZE})
return
for addr, desc, start, length in partitions:
end = start + length - 1
size_in_bytes = length * SECTOR_SIZE
readable_size = self.image_handler.get_readable_size(size_in_bytes)
fs_type = self.image_handler.get_fs_type(start)
desc_str = desc.decode('utf-8') if isinstance(desc, bytes) else desc
item_text = f"vol{addr} ({desc_str}: {start}-{end}, Size: {readable_size}, FS: {fs_type})"
icon_path = self.db_manager.get_icon_path('device', 'drive-harddisk')
data = {"inode_number": None, "start_offset": start, "end_offset": end}
item = self.create_tree_item(root_item_tree, item_text, icon_path, data)
# Determine if the partition is special or contains unallocated space
special_partitions = ["Primary Table", "Safety Table", "GPT Header"]
is_special = any(special_case in desc_str for special_case in special_partitions)
is_unallocated = "Unallocated" in desc_str or "Microsoft reserved" in desc_str
if is_special:
item.setChildIndicatorPolicy(QTreeWidgetItem.DontShowIndicator)
elif is_unallocated:
item.setChildIndicatorPolicy(QTreeWidgetItem.ShowIndicator)
# Directly add unallocated space under the partition
self.create_tree_item(item, f"Unallocated Space: Size: {readable_size}",
self.db_manager.get_icon_path('file', 'unknown'),
{"is_unallocated": True, "start_offset": start, "end_offset": end})
else:
if self.image_handler.check_partition_contents(start):
item.setChildIndicatorPolicy(QTreeWidgetItem.ShowIndicator)
else:
item.setChildIndicatorPolicy(QTreeWidgetItem.DontShowIndicator)
def populate_contents(self, item: QTreeWidgetItem, data: Dict[str, Any], inode: Optional[int] = None) -> None:
"""Populate tree widget item with directory contents."""
if self.current_image_path is None:
return
entries = self.image_handler.get_directory_contents(data["start_offset"], inode)
for entry in entries:
self._create_tree_item_for_entry(item, entry, data["start_offset"])
def on_item_expanded(self, item):
# Check if the item already has children; if so, don't repopulate
if item.childCount() > 0:
return
data = item.data(0, Qt.UserRole)
if data is None:
return
if data.get("inode_number") is None: # It's a partition
self.populate_contents(item, data)
else: # It's a directory
self.populate_contents(item, data, data.get("inode_number"))
class FileContentWorker(QThread):
"""Worker thread class for handling file operations in the background."""
completed = Signal(bytes, object)
error = Signal(str)
def __init__(self, image_handler, inode_number, offset):
super().__init__()
self.image_handler = image_handler
self.inode_number = inode_number
self.offset = offset
def run(self):
try:
file_content, metadata = self.image_handler.get_file_content(self.inode_number, self.offset)
if file_content:
self.completed.emit(file_content, metadata)
else:
self.error.emit("Unable to read file content.")
except Exception as e:
self.error.emit(f"Error reading file: {str(e)}")
# Worker thread for opening media files for streaming (doesn't load content into memory)
class MediaStreamWorker(QThread):
completed = Signal(object, int, object) # file_obj, file_size, metadata
error = Signal(str)
def __init__(self, image_handler, inode_number, offset):
super().__init__()
self.image_handler = image_handler
self.inode_number = inode_number
self.offset = offset
def run(self):
try:
# Get filesystem info
fs = self.image_handler.get_fs_info(self.offset)
if not fs:
self.error.emit("Unable to get filesystem info.")
return
# Open the file object (don't read content)
file_obj = fs.open_meta(inode=self.inode_number)
if not file_obj:
self.error.emit("Unable to open file.")
return
file_size = file_obj.info.meta.size
metadata = file_obj.info.meta
if file_size == 0:
self.error.emit("File has no content or is a special metafile!")
return
# Return the file object for streaming (don't read content)
self.completed.emit(file_obj, file_size, metadata)
except Exception as e:
self.error.emit(f"Error opening file for streaming: {str(e)}")
# Create a worker thread class for handling unallocated space operations in the background
class UnallocatedSpaceWorker(QThread):
completed = Signal(bytes)
error = Signal(str)
def __init__(self, image_handler, start_offset, end_offset):
super().__init__()
self.image_handler = image_handler
self.start_offset = start_offset
self.end_offset = end_offset
def run(self):
try:
unallocated_space = self.image_handler.read_unallocated_space(self.start_offset, self.end_offset)
if unallocated_space:
self.completed.emit(unallocated_space)
else:
self.error.emit("Unable to read unallocated space.")
except Exception as e:
self.error.emit(f"Error reading unallocated space: {str(e)}")
def on_item_clicked(self, item, column):
self.clear_viewers()
data = item.data(0, Qt.UserRole)
if not data:
return
# Store the current selection data
self.current_selected_data = data
# Show a status message in the UI to indicate loading
statusbar = self.statusBar()
statusbar.showMessage("Loading content...")
# Use a background worker thread if processing large files or unallocated space
try:
# Check if this is the root disk image item (has start_offset but no type/inode)
if (data.get("start_offset") == 0 and
not data.get("type") and
not data.get("inode_number") and
not data.get("is_unallocated")):
# This is the root disk image - display all volumes/partitions
self.display_volumes_in_listing()
statusbar.clearMessage()
return
if data.get("is_unallocated"):
# Handle unallocated space in background
self.unallocated_worker = self.UnallocatedSpaceWorker(
self.image_handler, data["start_offset"], data["end_offset"])
self.unallocated_worker.completed.connect(
lambda content: self.update_viewer_with_file_content(content, data))
self.unallocated_worker.error.connect(
lambda msg: (self.log_error(msg), statusbar.clearMessage()))
self.unallocated_worker.start()
elif data.get("type") == "directory":
# For directories, find parent inode to enable up navigation
if "parent_inode" not in data and data.get("inode_number"):
parent_inode = self.find_parent_inode(data["start_offset"], data["inode_number"])
if parent_inode:
data["parent_inode"] = parent_inode
# Update the stored data with parent information
self.current_selected_data = data
# Handle directories - populate the listing synchronously
entries = self.image_handler.get_directory_contents(data["start_offset"], data.get("inode_number"))
# Update current path for directory navigation
if data.get("name"):
if data.get("inode_number") == 5: # Root directory
self.current_path = "/"
else:
# If it's a regular directory, update the path
self.current_path = data.get("path", os.path.join(self.current_path, data.get("name", "")))
# Update directory up button state
self.update_directory_up_button()
# Populate the listing table with directory contents
self.populate_listing_table(entries, data["start_offset"])
# Add to navigation history
self._add_to_history(data)
statusbar.clearMessage()
elif data.get("inode_number") is not None:
# Handle files in background
self.file_worker = self.FileContentWorker(
self.image_handler, data["inode_number"], data["start_offset"])
self.file_worker.completed.connect(
lambda content, _: self.update_viewer_with_file_content(content, data))
self.file_worker.error.connect(
lambda msg: (self.log_error(msg), statusbar.clearMessage()))
self.file_worker.start()
elif data.get("start_offset") is not None:
# Handle partitions
entries = self.image_handler.get_directory_contents(data["start_offset"],
5) # 5 is the root inode for NTFS
# Reset path to root when viewing partitions
self.current_path = "/"
# Treat partition as a volume for history
if "type" not in data:
data["type"] = "volume"
if "inode_number" not in data:
data["inode_number"] = 5
self.populate_listing_table(entries, data["start_offset"])
# Add to navigation history
self._add_to_history(data)
statusbar.clearMessage()
else:
self.log_error("Clicked item is not a file, directory, or unallocated space.")
statusbar.clearMessage()
except Exception as e:
self.log_error(f"Error processing item: {str(e)}")
statusbar.clearMessage()
def update_directory_up_button(self):
"""Update the state of the directory up button based on current selection"""
if not self.current_selected_data:
self.go_up_action.setEnabled(False)
return
# Check if this is a directory
if self.current_selected_data.get("type") == "directory":
inode_number = self.current_selected_data.get("inode_number")
start_offset = self.current_selected_data.get("start_offset")
# Check if this is root directory (inode 5 in NTFS)
is_root = inode_number == 5
# If parent_inode isn't set yet, try to find it
if "parent_inode" not in self.current_selected_data and not is_root and inode_number is not None:
parent_inode = self.find_parent_inode(start_offset, inode_number)
if parent_inode:
# Update the dictionary in place
self.current_selected_data["parent_inode"] = parent_inode
has_parent = self.current_selected_data.get("parent_inode") is not None
self.go_up_action.setEnabled(not is_root and has_parent)
else:
self.go_up_action.setEnabled(False)
def find_parent_inode(self, start_offset, inode_number):
"""Helper method to find the parent inode for a directory from tree view"""
try:
# Root directory (5 is typically root in NTFS) has no parent
if inode_number == 5:
return None
# Get directory entries for the directory
entries = self.image_handler.get_directory_contents(start_offset, inode_number)
# Look for parent directory entry (..)
for entry in entries:
if entry.get("name") == "..":
return entry.get("inode_number")
# If we can't find the proper parent, return None
return None
except Exception as e:
self.log_error(f"Error finding parent inode: {str(e)}")
return None
def navigate_up_directory(self):
"""Navigate to the parent directory"""
if not self.current_selected_data:
return
# Ensure we have valid information
start_offset = self.current_selected_data.get("start_offset")
inode_number = self.current_selected_data.get("inode_number")
if not start_offset or not inode_number:
return
# If parent_inode isn't already set, try to find it
if "parent_inode" not in self.current_selected_data and self.current_selected_data.get("type") == "directory":
parent_inode = self.find_parent_inode(start_offset, inode_number)
if parent_inode:
self.current_selected_data["parent_inode"] = parent_inode
# Make sure we have a parent to navigate to
parent_inode = self.current_selected_data.get("parent_inode")
if not parent_inode:
return
statusbar = self.statusBar()
statusbar.showMessage("Loading parent directory...")
try:
# Create data for parent directory
parent_data = {
"inode_number": parent_inode,
"start_offset": start_offset,
"type": "directory",
# Get the grandparent inode if available (for consecutive up navigation)
"parent_inode": self.get_grandparent_inode(parent_inode, start_offset)
}
# Update current path (navigate to parent directory)
self.current_path = os.path.dirname(self.current_path)
if self.current_path == "":
self.current_path = "/"
# Load the parent directory
entries = self.image_handler.get_directory_contents(
parent_data["start_offset"],
parent_data["inode_number"]
)
self.current_selected_data = parent_data
# Update directory up button state
self.update_directory_up_button()
# Update both the tree view selection and listing table
self.populate_listing_table(entries, parent_data["start_offset"])
# Add to navigation history
self._add_to_history(parent_data)
# Find and select the corresponding item in the tree view if possible
self.select_tree_item_by_inode(parent_data["inode_number"], parent_data["start_offset"])
statusbar.clearMessage()
except Exception as e:
self.log_error(f"Error navigating to parent directory: {str(e)}")
statusbar.clearMessage()
def _add_to_history(self, directory_data):
"""Add a directory to the navigation history."""
# Skip if we're navigating through history
if self._navigating_history:
return
# Only add directories to history (not files)
if directory_data.get("type") != "directory" and directory_data.get("type") != "volume":
return
# Create a history entry with essential data
history_entry = {
"inode_number": directory_data.get("inode_number"),
"start_offset": directory_data.get("start_offset"),
"type": directory_data.get("type"),
"name": directory_data.get("name"),
"path": self.current_path,
"parent_inode": directory_data.get("parent_inode")
}
# If we're in the middle of history (not at the end), remove everything after current position
if self._history_index < len(self._directory_history) - 1:
self._directory_history = self._directory_history[:self._history_index + 1]
# Add new entry to history
self._directory_history.append(history_entry)
self._history_index = len(self._directory_history) - 1
# Update navigation buttons
self._update_navigation_buttons()
def _update_navigation_buttons(self):
"""Update the enabled state of Back/Forward navigation buttons."""
# Enable Back button if we can go back
can_go_back = self._history_index > 0
self.back_action.setEnabled(can_go_back)
# Enable Forward button if we can go forward
can_go_forward = self._history_index < len(self._directory_history) - 1
self.forward_action.setEnabled(can_go_forward)
def navigate_back(self):
"""Navigate to the previous directory in history."""
if self._history_index <= 0:
return
try:
# Set flag to prevent adding to history
self._navigating_history = True
# Move back in history
self._history_index -= 1
history_entry = self._directory_history[self._history_index]
# Navigate to the directory
self._navigate_to_history_entry(history_entry)
finally:
# Always clear the flag
self._navigating_history = False
self._update_navigation_buttons()
def navigate_forward(self):
"""Navigate to the next directory in history."""
if self._history_index >= len(self._directory_history) - 1:
return
try:
# Set flag to prevent adding to history
self._navigating_history = True
# Move forward in history
self._history_index += 1
history_entry = self._directory_history[self._history_index]
# Navigate to the directory
self._navigate_to_history_entry(history_entry)
finally:
# Always clear the flag
self._navigating_history = False
self._update_navigation_buttons()
def _navigate_to_history_entry(self, history_entry):
"""Navigate to a specific directory from history."""
statusbar = self.statusBar()
statusbar.showMessage("Navigating...")
try:
# Restore the path
self.current_path = history_entry.get("path", "/")
# Get directory contents
inode_number = history_entry.get("inode_number")
start_offset = history_entry.get("start_offset")
if history_entry.get("type") == "volume":
# For volumes, get root directory (inode 5)
entries = self.image_handler.get_directory_contents(start_offset, 5)
else:
# For regular directories, use stored inode
entries = self.image_handler.get_directory_contents(start_offset, inode_number)
# Update current selected data
self.current_selected_data = history_entry.copy()
# Update directory up button state
self.update_directory_up_button()
# Populate the listing table
self.populate_listing_table(entries, start_offset)
# Find and select the corresponding item in the tree view if possible
if inode_number:
self.select_tree_item_by_inode(inode_number, start_offset)
statusbar.clearMessage()
except Exception as e:
self.log_error(f"Error navigating from history: {str(e)}")
statusbar.clearMessage()
def select_tree_item_by_inode(self, inode_number, start_offset):
"""Attempt to find and select the item in the tree view that matches the given inode"""
try:
# Skip if inode_number is None
if inode_number is None:
return
# Get the root items
root_item = self.tree_viewer.invisibleRootItem()
# Find the item with matching inode and start_offset (recursive search)
found_item = self.find_tree_item_recursive(root_item, inode_number, start_offset)
if found_item:
# Temporarily disconnect the item clicked signal to prevent loops
self.tree_viewer.itemClicked.disconnect(self.on_item_clicked)
# Select the item and make it visible
self.tree_viewer.setCurrentItem(found_item)
self.tree_viewer.scrollToItem(found_item)
# Reconnect the signal
self.tree_viewer.itemClicked.connect(self.on_item_clicked)
except Exception as e:
self.log_error(f"Error selecting tree item: {str(e)}")
def find_tree_item_recursive(self, parent_item, inode_number, start_offset):
"""Recursively search for a tree item with matching inode and start_offset"""
# Check all children of the parent item
for i in range(parent_item.childCount()):
item = parent_item.child(i)
data = item.data(0, Qt.UserRole)
# Check if this item matches (allow matching based on inode only)
if data and data.get("inode_number") == inode_number:
# If start_offset is also provided and doesn't match, continue searching
if start_offset is not None and data.get("start_offset") != start_offset:
continue
return item
# If it has children, search recursively
if item.childCount() > 0:
found = self.find_tree_item_recursive(item, inode_number, start_offset)
if found:
return found
# Not found
return None
def display_volumes_in_listing(self) -> None:
"""Display all volumes/partitions in the listing table when disk image root is clicked."""
# Clear existing content
self.listing_table.setRowCount(0)
self.listing_table.setSortingEnabled(False)
# Show columns with volume information, hide file-specific columns
self.listing_table.setColumnHidden(1, False) # Show Inode (for Volume #)
self.listing_table.setColumnHidden(4, False) # Show Created (for Start Offset)
self.listing_table.setColumnHidden(5, False) # Show Accessed (for End Offset)
self.listing_table.setColumnHidden(6, False) # Show Modified (for Length)
self.listing_table.setColumnHidden(7, False) # Show Changed (for Block Size)
self.listing_table.setColumnHidden(8, True) # Hide Path (not relevant for volumes)
self.listing_table.setColumnHidden(9, False) # Show Info (for additional details)
# Update column headers for volume context
self.listing_table.setHorizontalHeaderLabels([
'Name', 'Volume #', 'Type', 'Size', 'Start Offset', 'End Offset',
'Length', 'Block Size', 'Path', 'Details'
])
# Make Info column much wider for detailed information
self.listing_table.setColumnWidth(9, 1200)
# Reset path to root
self.current_path = "/"
# Clear navigation history when returning to disk image root
self._directory_history = []
self._history_index = -1
self._update_navigation_buttons()
# Disable the up button since we're at the disk image root
self.go_up_action.setEnabled(False)
# Get all partitions
partitions = self.image_handler.get_partitions()
if not partitions:
# No partitions found
self.listing_table.setSortingEnabled(True)
return
try:
for addr, desc, start, length in partitions:
row_position = self.listing_table.rowCount()
self.listing_table.insertRow(row_position)
# Calculate volume information
end = start + length - 1
size_in_bytes = length * SECTOR_SIZE
readable_size = self.image_handler.get_readable_size(size_in_bytes)
fs_type = self.image_handler.get_fs_type(start)
desc_str = desc.decode('utf-8') if isinstance(desc, bytes) else desc
# Get additional filesystem details
try:
fs_info = self.image_handler.get_fs_info(start)
if fs_info and hasattr(fs_info.info, 'block_size'):
block_size = f"{fs_info.info.block_size:,} bytes"
else:
block_size = "N/A"
except:
block_size = "N/A"
# Volume name
volume_name = f"vol{addr}"
name_item = QTableWidgetItem(volume_name)
icon_path = self.db_manager.get_icon_path('device', 'drive-harddisk')
name_item.setIcon(QIcon(icon_path))
# Store volume data for potential future use
volume_data = {
"name": volume_name,
"type": "volume",
"start_offset": start,
"end_offset": end,
"addr": addr,
"description": desc_str,
"filesystem": fs_type
}
name_item.setData(Qt.UserRole, volume_data)
# Create table items with detailed information
inode_item = QTableWidgetItem(str(addr)) # Volume number in Inode column
type_item = QTableWidgetItem(fs_type)
size_item = QTableWidgetItem(readable_size)
# Use timestamp columns for partition geometry
start_offset_item = QTableWidgetItem(f"{start:,} sectors")
end_offset_item = QTableWidgetItem(f"{end:,} sectors")
length_item = QTableWidgetItem(f"{length:,} sectors")
block_size_item = QTableWidgetItem(block_size)
# Build comprehensive info string
info_parts = []
# Add description first without label if it exists
if desc_str and desc_str.strip():
info_parts.append(desc_str)
# Add detailed partition information
info_parts.append(f"Start: {start:,} sectors ({start * SECTOR_SIZE:,} bytes)")
info_parts.append(f"End: {end:,} sectors ({end * SECTOR_SIZE:,} bytes)")
info_parts.append(f"Length: {length:,} sectors ({size_in_bytes:,} bytes)")
if block_size != "N/A":
info_parts.append(f"Block Size: {block_size}")
info_parts.append(f"Filesystem: {fs_type}")
info_item = QTableWidgetItem(" | ".join(info_parts))
# Set items in table
self.listing_table.setItem(row_position, 0, name_item)
self.listing_table.setItem(row_position, 1, inode_item)
self.listing_table.setItem(row_position, 2, type_item)
self.listing_table.setItem(row_position, 3, size_item)
self.listing_table.setItem(row_position, 4, start_offset_item)
self.listing_table.setItem(row_position, 5, end_offset_item)
self.listing_table.setItem(row_position, 6, length_item)
self.listing_table.setItem(row_position, 7, block_size_item)
self.listing_table.setItem(row_position, 9, info_item)
finally:
self.listing_table.setSortingEnabled(True)
def populate_listing_table(self, entries: List[Dict[str, Any]], offset: int) -> None:
"""Populate the listing table with directory entries in batches for better performance."""
# Clear existing content
self.listing_table.setRowCount(0)
# Restore original column headers for file/folder view
self.listing_table.setHorizontalHeaderLabels([
'Name', 'Inode', 'Type', 'Size', 'Created Date', 'Accessed Date',
'Modified Date', 'Changed Date', 'Path', 'Info'
])
# Show columns relevant for files/folders, hide Info column
self.listing_table.setColumnHidden(1, False) # Show Inode
self.listing_table.setColumnHidden(4, False) # Show Created
self.listing_table.setColumnHidden(5, False) # Show Accessed
self.listing_table.setColumnHidden(6, False) # Show Modified
self.listing_table.setColumnHidden(7, False) # Show Changed
self.listing_table.setColumnHidden(8, False) # Show Path
self.listing_table.setColumnHidden(9, True) # Hide Info
if not entries:
return
# Enable/disable the up button based on whether we're in the root directory
self.update_directory_up_button()
# Disable sorting and updates for better performance during bulk population
self.listing_table.setSortingEnabled(False)
self.listing_table.setUpdatesEnabled(False)
try:
total_entries = len(entries)
# Process in batches to keep UI responsive
for batch_start in range(0, total_entries, TABLE_BATCH_SIZE):
batch_end = min(batch_start + TABLE_BATCH_SIZE, total_entries)
batch = entries[batch_start:batch_end]
# Populate the batch
for entry in batch:
row_position = self.listing_table.rowCount()
self._populate_table_entry(row_position, entry, offset)
# Process events periodically to keep UI responsive
if batch_end < total_entries:
QApplication.processEvents()
finally:
# Re-enable updates and sorting
self.listing_table.setUpdatesEnabled(True)
self.listing_table.setSortingEnabled(True)
def insert_row_into_listing_table(self, entry_name, entry_inode, description, icon_name, icon_type, offset, size,
created, accessed, modified, changed, parent_inode=None):
"""Insert a row into the listing table with proper caching and error handling."""
try:
icon_path = self.db_manager.get_icon_path(icon_type, icon_name)
icon = QIcon(icon_path)
row_position = self.listing_table.rowCount() - 1 # Current row (rows are 0-indexed)
# Calculate the full path for this item
file_path = os.path.join(self.current_path, entry_name) if entry_name != ".." else os.path.dirname(
self.current_path)
name_item = QTableWidgetItem(entry_name)
name_item.setIcon(icon)
name_item.setData(Qt.UserRole, {
"inode_number": entry_inode,
"start_offset": offset,
"type": "directory" if icon_type == 'folder' else 'file',
"name": entry_name,
"size": size,
"parent_inode": parent_inode, # Store parent directory inode for "Go Up" functionality
"path": file_path # Store the full path
})
self.listing_table.setItem(row_position, 0, name_item)
self.listing_table.setItem(row_position, 1, QTableWidgetItem(str(entry_inode)))
self.listing_table.setItem(row_position, 2, QTableWidgetItem(description))
self.listing_table.setItem(row_position, 3, QTableWidgetItem(str(size)))
self.listing_table.setItem(row_position, 4, QTableWidgetItem(str(created)))
self.listing_table.setItem(row_position, 5, QTableWidgetItem(str(accessed)))
self.listing_table.setItem(row_position, 6, QTableWidgetItem(str(modified)))
self.listing_table.setItem(row_position, 7, QTableWidgetItem(str(changed)))
self.listing_table.setItem(row_position, 8, QTableWidgetItem(file_path))
self.listing_table.setItem(row_position, 9, QTableWidgetItem("")) # Empty Info column for files/folders
except Exception as e:
self.log_error(f"Error adding row to listing table: {str(e)}")
# Try to recover by removing the incomplete row
try:
if row_position >= 0:
self.listing_table.removeRow(row_position)
except:
pass
def update_viewer_with_file_content(self, file_content, data):
"""Update the active viewer tab with the file content.
This method is called after file content is loaded, either directly
or from a background thread.
"""
# Clear the status message if it exists
statusbar = self.statusBar()
statusbar.clearMessage()
# Get the active tab index
index = self.viewer_tab.currentIndex()
if not file_content:
self.log_error("No content available to display")
return
# Use optimized display methods for each viewer type
try:
if index == 0: # Hex tab
self.hex_viewer.display_hex_content(file_content)
elif index == 1: # Text tab
self.text_viewer.display_text_content(file_content)
elif index == 2: # Application tab
full_file_path = data.get("name", "") # Retrieve the name from the data dictionary
self.application_viewer.display_application_content(file_content, full_file_path)
elif index == 3: # File Metadata tab
self.metadata_viewer.display_metadata(data)
elif index == 4: # Exif Data tab
self.exif_viewer.load_and_display_exif_data(file_content)
elif index == 5: # Assuming VirusTotal tab is the 6th tab (0-based index)
file_hash = hashlib.md5(file_content).hexdigest()
self.virus_total_api.set_file_hash(file_hash)
self.virus_total_api.set_file_content(file_content, data.get("name", ""))
except Exception as e:
self.log_error(f"Error displaying content in viewer: {str(e)}")
def update_viewer_with_media_stream(self, file_obj, file_size, metadata, data):
"""Update the application viewer with a media stream for playback."""
# Clear the status message if it exists
statusbar = self.statusBar()
statusbar.clearMessage()
try:
# Determine MIME type from file extension
full_file_path = data.get("name", "")
file_extension = os.path.splitext(full_file_path)[-1].lower()
# Map extension to MIME type
mime_type = None
if file_extension in ['.mp3', '.wav', '.ogg', '.aac', '.m4a']:
mime_type = f'audio/{file_extension[1:]}'
elif file_extension in ['.mp4', '.mkv', '.flv', '.avi', '.mov', '.webm', '.wmv', '.m4v']:
mime_type = 'video/mp4'
else:
mime_type = 'application/octet-stream'
# Call the load method with streaming parameters
self.application_viewer.load(
mime_type=mime_type,
path=full_file_path,
file_obj=file_obj,
file_size=file_size
)
except Exception as e:
self.log_error(f"Error setting up media stream: {str(e)}")
def display_content_for_active_tab(self):
"""Display content appropriate for the currently active tab."""
if not self.current_selected_data:
return
statusbar = self.statusBar()
statusbar.showMessage("Updating view...")
try:
# IMPORTANT: Cancel any running workers before starting new ones
# This prevents race conditions when switching between files
if hasattr(self, 'media_worker') and self.media_worker and self.media_worker.isRunning():
try:
# Disconnect signals to prevent callbacks
self.media_worker.completed.disconnect()
self.media_worker.error.disconnect()
# Request interruption (graceful)
self.media_worker.requestInterruption()
# Don't wait - let it finish naturally
except Exception as e:
print(f"Error cancelling media worker: {e}")
if hasattr(self, 'file_worker') and self.file_worker and self.file_worker.isRunning():
try:
# Disconnect signals to prevent callbacks
self.file_worker.completed.disconnect()
self.file_worker.error.disconnect()
# Request interruption (graceful)
self.file_worker.requestInterruption()
# Don't wait - let it finish naturally
except Exception as e:
print(f"Error cancelling file worker: {e}")
inode_number = self.current_selected_data.get("inode_number")
offset = self.current_selected_data.get("start_offset", self.current_offset)
if inode_number:
# Check if the active tab is Application tab (index 2) and file is audio/video
current_tab_index = self.viewer_tab.currentIndex()
file_name = self.current_selected_data.get("name", "")
file_extension = os.path.splitext(file_name)[-1].lower()
# Media file extensions
media_extensions = ['.mp3', '.wav', '.ogg', '.aac', '.m4a', '.mp4', '.mkv',
'.flv', '.avi', '.mov', '.webm', '.wmv', '.m4v']
# Use streaming for media files on Application tab
if current_tab_index == 2 and file_extension in media_extensions:
# Use MediaStreamWorker for streaming playback (doesn't load content)
self.media_worker = self.MediaStreamWorker(self.image_handler, inode_number, offset)
self.media_worker.completed.connect(
lambda file_obj, file_size, metadata: self.update_viewer_with_media_stream(
file_obj, file_size, metadata, self.current_selected_data))
self.media_worker.error.connect(
lambda msg: (self.log_error(msg), statusbar.clearMessage()))
self.media_worker.start()
else:
# For non-media files or other tabs, use FileContentWorker (loads content)
self.file_worker = self.FileContentWorker(self.image_handler, inode_number, offset)
self.file_worker.completed.connect(
lambda content, _: self.update_viewer_with_file_content(content, self.current_selected_data))
self.file_worker.error.connect(
lambda msg: (self.log_error(msg), statusbar.clearMessage()))
self.file_worker.start()
else:
statusbar.clearMessage()
except Exception as e:
self.log_error(f"Error updating active tab: {str(e)}")
statusbar.clearMessage()
def open_listing_context_menu(self, position):
# Get the selected item
indexes = self.listing_table.selectedIndexes()
if indexes:
selected_item = self.listing_table.item(indexes[0].row(),
0) # Assuming the first column contains the item data
data = selected_item.data(Qt.UserRole)
menu = QMenu()
# If in search mode and item is a file, add "Open File" and "Show in Directory"
if self._search_mode and data.get('type') == 'file':
# Open File action
open_action = menu.addAction("Open File")
open_action.triggered.connect(lambda: self.open_search_result_file(data))
# Show in Directory action
show_in_dir_action = menu.addAction("Show in Directory")
show_in_dir_action.triggered.connect(lambda: self.show_file_in_directory(data))
# Add separator
menu.addSeparator()
# Add the 'Export' option for any file or folder
export_action = menu.addAction("Export")
export_action.triggered.connect(lambda: self.handle_export(data, QFileDialog.getExistingDirectory(self,
"Select Destination Directory")))
menu.exec_(self.listing_table.viewport().mapToGlobal(position))
def handle_export(self, data, dest_dir):
"""Export the selected item in a background thread with progress display."""
if not dest_dir:
return
try:
# Create a progress dialog
progress_dialog = QProgressDialog("Preparing to export...", "Cancel", 0, 100, self)
progress_dialog.setWindowTitle("Exporting Files")
progress_dialog.setWindowModality(Qt.WindowModal)
progress_dialog.setMinimumDuration(0)
progress_dialog.setValue(0)
progress_dialog.show()
# Create and configure the worker
self.export_worker = ExportWorker(
self.image_handler,
data["inode_number"],
data["start_offset"],
dest_dir,
data["name"],
data["type"] == "directory"
)
# Connect worker signals
self.export_worker.progress.connect(
lambda current, total: progress_dialog.setValue(int(current * 100 / total) if total > 0 else 0)
)
self.export_worker.status_update.connect(progress_dialog.setLabelText)
self.export_worker.error.connect(lambda msg: QMessageBox.warning(self, "Export Error", msg))
self.export_worker.finished.connect(progress_dialog.close)
# Connect the cancel button
progress_dialog.canceled.connect(self.export_worker.terminate)
# Start the worker
self.export_worker.start()
except Exception as e:
QMessageBox.critical(self, "Export Error", f"Error starting export: {str(e)}")
def log_error(self, message):
"""Log an error message to the console and potentially to a log file."""
logger.error(f"Error: {message}")
# Could also log to a file or status bar here
def open_tree_context_menu(self, position):
# Get the selected item
indexes = self.tree_viewer.selectedIndexes()
if indexes:
selected_item = self.tree_viewer.itemFromIndex(indexes[0])
menu = QMenu()
data = selected_item.data(0, Qt.UserRole)
# Check if the selected item is a root item (disk image)
if selected_item and selected_item.parent() is None:
view_os_info_action = menu.addAction("View Image Information")
view_os_info_action.triggered.connect(lambda: self.view_os_information(indexes[0]))
# Add the 'Export' option for any file or folder
export_action = menu.addAction("Export")
export_action.triggered.connect(
lambda: self.handle_export(self.tree_viewer.itemFromIndex(indexes[0]).data(0, Qt.UserRole),
QFileDialog.getExistingDirectory(self, "Select Destination Directory")))
menu.exec_(self.tree_viewer.viewport().mapToGlobal(position))
def view_os_information(self, index):
"""Display comprehensive disk image information with space allocation pie chart."""
item = self.tree_viewer.itemFromIndex(index)
if item is None or item.parent() is not None:
# Ensure that only the root item triggers the information display
return
# Create modern dialog
dialog = QDialog(self)
dialog.setWindowTitle("Disk Image Information")
dialog.resize(1200, 800)
# Main vertical layout
main_layout = QVBoxLayout(dialog)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
# === TOP SECTION: Image Overview with Chart ===
top_widget = QWidget()
top_widget.setStyleSheet("background-color: #f8f9fa; border-bottom: 2px solid #dee2e6;")
top_layout = QHBoxLayout(top_widget)
top_layout.setContentsMargins(20, 20, 20, 20)
top_layout.setSpacing(30)
# Left: Image Summary Card
summary_card = QWidget()
summary_card.setStyleSheet("""
QWidget {
background-color: white;
border-radius: 8px;
border: 1px solid #dee2e6;
}
""")
summary_layout = QVBoxLayout(summary_card)
summary_layout.setContentsMargins(20, 20, 20, 20)
summary_layout.setSpacing(12)
# Title
title_label = QLabel("Disk Image Overview")
title_label.setStyleSheet("font-size: 16pt; font-weight: bold; color: #212529; border: none;")
summary_layout.addWidget(title_label)
# Key info
image_info = self._get_image_info()
key_fields = ["Image Path", "Image Type", "Total Size", "Partition Scheme", "Number of Partitions", "Status"]
for field in key_fields:
if field in image_info:
info_row = QWidget()
info_row.setStyleSheet("border: none;")
info_row_layout = QHBoxLayout(info_row)
info_row_layout.setContentsMargins(0, 0, 0, 0)
info_row_layout.setSpacing(10)
label = QLabel(f"{field}:")
label.setStyleSheet("font-weight: bold; color: #495057; font-size: 10pt; border: none;")
label.setMinimumWidth(140)
value = QLabel(str(image_info[field]))
value.setStyleSheet("color: #212529; font-size: 10pt; border: none;")
value.setTextInteractionFlags(Qt.TextSelectableByMouse)
value.setWordWrap(True)
info_row_layout.addWidget(label)
info_row_layout.addWidget(value, 1)
summary_layout.addWidget(info_row)
summary_layout.addStretch()
summary_card.setFixedWidth(450)
# Right: Pie Chart with Legend
chart_widget = QWidget()
chart_widget.setStyleSheet("""
QWidget {
background-color: white;
border-radius: 8px;
border: 1px solid #dee2e6;
}
""")
chart_outer_layout = QVBoxLayout(chart_widget)
chart_outer_layout.setContentsMargins(15, 15, 15, 15)
chart_outer_layout.setSpacing(10)
chart_title = QLabel("Space Allocation")
chart_title.setStyleSheet("font-size: 14pt; font-weight: bold; color: #212529; border: none;")
chart_title.setAlignment(Qt.AlignCenter)
chart_outer_layout.addWidget(chart_title)
# Create horizontal layout for legend (left) and chart (right)
chart_content_layout = QHBoxLayout()
chart_content_layout.setSpacing(15)
# Create chart
chart_view, partition_info_list = self._create_space_allocation_chart()
# Compact legend on the left
if partition_info_list:
legend_widget = QWidget()
legend_widget.setStyleSheet("border: none;")
legend_layout = QVBoxLayout(legend_widget)
legend_layout.setContentsMargins(5, 5, 5, 5)
legend_layout.setSpacing(6)
for label_text, color in partition_info_list:
legend_row = QWidget()
legend_row.setStyleSheet("border: none;")
legend_row_layout = QHBoxLayout(legend_row)
legend_row_layout.setContentsMargins(0, 0, 0, 0)
legend_row_layout.setSpacing(8)
color_indicator = QLabel()
color_indicator.setFixedSize(16, 16)
color_indicator.setStyleSheet(f"""
background-color: rgb({color.red()}, {color.green()}, {color.blue()});
border: 1px solid #adb5bd;
border-radius: 3px;
""")
text_label = QLabel(label_text)
text_label.setStyleSheet("color: #495057; font-size: 9pt; border: none;")
text_label.setWordWrap(True)
legend_row_layout.addWidget(color_indicator)
legend_row_layout.addWidget(text_label, 1)
legend_layout.addWidget(legend_row)
legend_layout.addStretch()
legend_widget.setMaximumWidth(300)
chart_content_layout.addWidget(legend_widget)
chart_content_layout.addWidget(chart_view, 1)
chart_outer_layout.addLayout(chart_content_layout, 1)
top_layout.addWidget(summary_card)
top_layout.addWidget(chart_widget, 1)
main_layout.addWidget(top_widget)
# === BOTTOM SECTION: Detailed Partition Information ===
bottom_widget = QWidget()
bottom_layout = QVBoxLayout(bottom_widget)
bottom_layout.setContentsMargins(20, 20, 20, 20)
bottom_layout.setSpacing(15)
# Section title
details_title = QLabel("Volume Details")
details_title.setStyleSheet("font-size: 14pt; font-weight: bold; color: #212529; padding-bottom: 10px;")
bottom_layout.addWidget(details_title)
# Professional table view for volume information
volume_table = QTableWidget()
volume_table.setSortingEnabled(True)
volume_table.verticalHeader().setVisible(False)
volume_table.setObjectName("volumeInfoTable")
volume_table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
volume_table.setAlternatingRowColors(True)
volume_table.setEditTriggers(QTableWidget.NoEditTriggers)
volume_table.setIconSize(QSize(24, 24))
volume_table.setSelectionBehavior(QTableWidget.SelectRows)
# Enable horizontal scrolling for smaller windows
volume_table.setHorizontalScrollMode(QTableWidget.ScrollPerPixel)
volume_table.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
# Set column count and headers
volume_table.setColumnCount(10)
volume_table.setHorizontalHeaderLabels([
'Volume', 'Filesystem', 'Offset (Sectors)', 'Block Size', 'Volume Size',
'Total Blocks', 'First Block', 'Last Block', 'Inode Count', 'Root Inode'
])
# Configure header - all columns use Interactive mode for horizontal scrolling
header = volume_table.horizontalHeader()
for i in range(10):
header.setSectionResizeMode(i, QHeaderView.Interactive)
# Set column widths
volume_table.setColumnWidth(0, 100) # Volume
volume_table.setColumnWidth(1, 120) # Filesystem
volume_table.setColumnWidth(2, 140) # Offset
volume_table.setColumnWidth(3, 100) # Block Size
volume_table.setColumnWidth(4, 120) # Volume Size
volume_table.setColumnWidth(5, 120) # Total Blocks
volume_table.setColumnWidth(6, 120) # First Block
volume_table.setColumnWidth(7, 120) # Last Block
volume_table.setColumnWidth(8, 120) # Inode Count
volume_table.setColumnWidth(9, 100) # Root Inode
# Set header alignment
header.setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter)
# Populate table with partition data
partitions = self.image_handler.get_partitions()
if partitions:
self._populate_volume_table(volume_table, partitions)
else:
# Show message in table if no partitions
volume_table.setRowCount(1)
no_part_item = QTableWidgetItem("No partitions detected or single filesystem image")
no_part_item.setForeground(QBrush(QColor(108, 117, 125)))
font = no_part_item.font()
font.setItalic(True)
no_part_item.setFont(font)
volume_table.setItem(0, 0, no_part_item)
volume_table.setSpan(0, 0, 1, 10)
bottom_layout.addWidget(volume_table, 1)
# Close button at bottom right
button_layout = QHBoxLayout()
button_layout.addStretch()
close_button = QPushButton("Close")
close_button.clicked.connect(dialog.accept)
close_button.setMinimumWidth(100)
button_layout.addWidget(close_button)
bottom_layout.addLayout(button_layout)
main_layout.addWidget(bottom_widget, 1)
dialog.exec_()
def _populate_volume_table(self, table, partitions):
"""Populate the volume table with partition information."""
table.setRowCount(len(partitions))
table.setSortingEnabled(False) # Disable sorting while populating
for idx, partition in enumerate(partitions):
addr, desc, start, length = partition
# Get volume information
volume_info = self._extract_comprehensive_volume_info(start)
# Combine all info
all_info = {}
all_info.update(volume_info["basic"])
all_info.update(volume_info["filesystem"])
# Get filesystem type for icon
fs_type = all_info.get("Filesystem Type", "Unknown")
icon_path = self.db_manager.get_icon_path('device', 'drive-harddisk')
# Column 0: Volume (with icon)
desc_str = desc.decode('utf-8') if isinstance(desc, bytes) else desc
volume_text = f"vol{addr}"
if desc_str and desc_str.strip():
volume_text += f" ({desc_str})"
volume_item = QTableWidgetItem(volume_text)
volume_item.setIcon(QIcon(icon_path))
table.setItem(idx, 0, volume_item)
# Column 1: Filesystem
fs_item = QTableWidgetItem(fs_type)
table.setItem(idx, 1, fs_item)
# Column 2: Offset (Sectors)
offset_value = all_info.get("Partition Offset", "N/A")
# Extract just the sector count
if "sectors" in offset_value:
offset_value = offset_value.split("sectors")[0].strip()
offset_item = QTableWidgetItem(offset_value)
table.setItem(idx, 2, offset_item)
# Column 3: Block Size
block_size = all_info.get("Block Size", "N/A")
block_size_item = QTableWidgetItem(block_size)
table.setItem(idx, 3, block_size_item)
# Column 4: Volume Size
volume_size = all_info.get("Volume Size", "N/A")
volume_size_item = QTableWidgetItem(volume_size)
table.setItem(idx, 4, volume_size_item)
# Column 5: Total Blocks
total_blocks = all_info.get("Total Blocks", "N/A")
total_blocks_item = QTableWidgetItem(total_blocks)
table.setItem(idx, 5, total_blocks_item)
# Column 6: First Block
first_block = all_info.get("First Block", "N/A")
first_block_item = QTableWidgetItem(first_block)
table.setItem(idx, 6, first_block_item)
# Column 7: Last Block
last_block = all_info.get("Last Block", "N/A")
last_block_item = QTableWidgetItem(last_block)
table.setItem(idx, 7, last_block_item)
# Column 8: Inode Count
inode_count = all_info.get("Inode Count", "N/A")
inode_count_item = QTableWidgetItem(inode_count)
table.setItem(idx, 8, inode_count_item)
# Column 9: Root Inode
root_inode = all_info.get("Root Inode", "N/A")
root_inode_item = QTableWidgetItem(root_inode)
table.setItem(idx, 9, root_inode_item)
table.setSortingEnabled(True) # Re-enable sorting after populating
def _extract_comprehensive_volume_info(self, start_offset):
"""Extract basic pytsk3 information from a volume."""
info = {
"basic": {},
"filesystem": {}
}
try:
# Get filesystem info
fs_info = self.image_handler.get_fs_info(start_offset)
if not fs_info:
info["basic"]["Status"] = "Unable to access filesystem"
return info
fs_type = self.image_handler.get_fs_type(start_offset)
# === BASIC INFO ===
info["basic"]["Partition Offset"] = f"{start_offset:,} sectors ({start_offset * 512:,} bytes)"
info["basic"]["Filesystem Type"] = fs_type or "Unknown"
if hasattr(fs_info.info, 'block_size'):
info["basic"]["Block Size"] = f"{fs_info.info.block_size:,} bytes"
if hasattr(fs_info.info, 'block_count'):
total_blocks = fs_info.info.block_count
total_size = total_blocks * fs_info.info.block_size
info["basic"]["Total Blocks"] = f"{total_blocks:,}"
info["basic"]["Volume Size"] = FileSystemUtils.get_readable_size(total_size)
# === FILESYSTEM DETAILS ===
if hasattr(fs_info.info, 'first_block'):
info["filesystem"]["First Block"] = f"{fs_info.info.first_block:,}"
if hasattr(fs_info.info, 'last_block'):
info["filesystem"]["Last Block"] = f"{fs_info.info.last_block:,}"
if hasattr(fs_info.info, 'inum_count'):
info["filesystem"]["Inode Count"] = f"{fs_info.info.inum_count:,}"
if hasattr(fs_info.info, 'root_inum'):
info["filesystem"]["Root Inode"] = f"{fs_info.info.root_inum}"
except Exception as e:
logger.error(f"Error extracting volume info: {e}")
info["basic"]["Error"] = str(e)
return info
def _get_image_info(self):
"""Extract comprehensive disk image information."""
info = {}
try:
# Basic image info
info["Image Path"] = self.image_handler.image_path
info["Image Type"] = self.image_handler.get_image_type().upper()
# Image size
total_size = self.image_handler.get_size()
info["Total Size"] = FileSystemUtils.get_readable_size(total_size)
info["Total Size (Bytes)"] = f"{total_size:,}"
# Sector information
sector_count = total_size // 512
info["Total Sectors"] = f"{sector_count:,}"
info["Bytes per Sector"] = "512"
# Volume information
if self.image_handler.volume_info:
try:
vol_type = self.image_handler.volume_info.info.vstype
volume_types = {
pytsk3.TSK_VS_TYPE_DOS: "DOS/MBR",
pytsk3.TSK_VS_TYPE_GPT: "GPT (GUID Partition Table)",
pytsk3.TSK_VS_TYPE_MAC: "Mac Partition Map",
pytsk3.TSK_VS_TYPE_BSD: "BSD Disk Label",
pytsk3.TSK_VS_TYPE_SUN: "Sun VTOC",
}
info["Partition Scheme"] = volume_types.get(vol_type, f"Unknown ({vol_type})")
info["Number of Partitions"] = len(self.image_handler.get_partitions())
except Exception as e:
logger.debug(f"Could not get volume type: {e}")
else:
info["Partition Scheme"] = "No partition table detected"
# Check if wiped
if self.image_handler.is_wiped():
info["Status"] = "⚠️ Wiped/Empty Image"
else:
info["Status"] = "✓ Valid Image"
# File modification time
if os.path.exists(self.image_handler.image_path):
mod_time = os.path.getmtime(self.image_handler.image_path)
info["File Modified"] = datetime.datetime.fromtimestamp(mod_time).strftime("%Y-%m-%d %H:%M:%S")
except Exception as e:
logger.error(f"Error getting image info: {e}")
info["Error"] = str(e)
return info
def _get_partition_info(self, partition):
"""Extract detailed partition information."""
info = {}
try:
addr, desc, start, length = partition
# Basic partition info (skip description as it's in the group box title)
info["Start Offset (Sectors)"] = f"{start:,}"
info["Start Offset (Bytes)"] = f"{start * 512:,}"
info["Length (Sectors)"] = f"{length:,}"
info["Length (Bytes)"] = f"{length * 512:,}"
info["Size"] = FileSystemUtils.get_readable_size(length * 512)
# Filesystem information
try:
fs_info = self.image_handler.get_fs_info(start)
if fs_info:
fs_type = self.image_handler.get_fs_type(start)
info["File System"] = fs_type or "Unknown"
# Block/cluster information
if hasattr(fs_info.info, 'block_size'):
info["Block Size"] = f"{fs_info.info.block_size:,} bytes"
if hasattr(fs_info.info, 'block_count'):
info["Block Count"] = f"{fs_info.info.block_count:,}"
# First and last block
if hasattr(fs_info.info, 'first_block'):
info["First Block"] = f"{fs_info.info.first_block:,}"
if hasattr(fs_info.info, 'last_block'):
info["Last Block"] = f"{fs_info.info.last_block:,}"
# Inode information
if hasattr(fs_info.info, 'inum_count'):
info["Inode Count"] = f"{fs_info.info.inum_count:,}"
if hasattr(fs_info.info, 'root_inum'):
info["Root Inode"] = f"{fs_info.info.root_inum}"
# OS detection for NTFS
if fs_type == "NTFS":
os_version = self.image_handler.get_windows_version(start)
if os_version:
info["Operating System"] = os_version
# Try to get volume label
try:
root_dir = fs_info.open_dir(path="/")
for entry in root_dir:
if hasattr(entry, 'info') and hasattr(entry.info, 'name'):
name = entry.info.name.name.decode('utf-8', errors='ignore')
if name in ["$VOLUME", "volume", ".volume"]:
# Found volume label
break
except:
pass
else:
info["File System"] = "Could not open filesystem"
except Exception as e:
info["File System"] = f"Error: {str(e)}"
logger.debug(f"Error getting filesystem info for partition: {e}")
except Exception as e:
logger.error(f"Error getting partition info: {e}")
info["Error"] = str(e)
return info
def _get_filesystem_colors(self):
"""Return consistent color mapping for filesystem types."""
return {
"NTFS": QColor(41, 128, 185), # Blue
"FAT32": QColor(46, 204, 113), # Green
"FAT16": QColor(26, 188, 156), # Turquoise
"FAT12": QColor(22, 160, 133), # Dark Turquoise
"exFAT": QColor(52, 152, 219), # Light Blue
"EXT4": QColor(231, 76, 60), # Red
"EXT3": QColor(192, 57, 43), # Dark Red
"EXT2": QColor(155, 89, 182), # Purple
"HFS+": QColor(241, 196, 15), # Yellow
"APFS": QColor(243, 156, 18), # Orange
"ISO9660": QColor(230, 126, 34), # Dark Orange
"Unallocated": QColor(149, 165, 166), # Gray
"Unknown": QColor(127, 140, 141), # Dark Gray
}
def _create_space_allocation_chart(self):
"""Create a pie chart showing allocated vs unallocated space."""
# Create pie series
series = QPieSeries()
legend_items = [] # Track items for legend
try:
total_size = self.image_handler.get_size()
partitions = self.image_handler.get_partitions()
# Get filesystem color mapping
fs_colors = self._get_filesystem_colors()
# Calculate allocated space (partitions)
allocated_space = 0
partition_details = []
if partitions:
for part in partitions:
addr, desc, start, length = part
size = length * 512
allocated_space += size
# Get filesystem type
fs_type = self.image_handler.get_fs_type(start)
if not fs_type:
fs_type = "Unknown"
partition_details.append((fs_type, size, part[0]))
# Calculate unallocated space
unallocated_space = total_size - allocated_space
# Add partition slices with consistent colors
for idx, (fs_type, size, part_num) in enumerate(partition_details):
percentage = (size / total_size) * 100
# Don't show label on slice - use legend instead
slice = series.append("", size)
# Use consistent color based on filesystem type
color = fs_colors.get(fs_type, fs_colors["Unknown"])
slice.setColor(color)
slice.setLabelVisible(False) # Hide labels on pie
# Add border between slices for clear separation
slice.setBorderColor(QColor(255, 255, 255))
slice.setBorderWidth(3)
# Add to legend with full details
legend_label = f"{fs_type} - Partition {part_num} ({FileSystemUtils.get_readable_size(size)}, {percentage:.1f}%)"
legend_items.append((legend_label, color))
# Add unallocated space
if unallocated_space > 0:
percentage = (unallocated_space / total_size) * 100
unalloc_slice = series.append("", unallocated_space)
unalloc_slice.setColor(fs_colors["Unallocated"])
unalloc_slice.setLabelVisible(False)
unalloc_slice.setBorderColor(QColor(255, 255, 255))
unalloc_slice.setBorderWidth(3)
# Add to legend
legend_label = f"Unallocated Space ({FileSystemUtils.get_readable_size(unallocated_space)}, {percentage:.1f}%)"
legend_items.append((legend_label, fs_colors["Unallocated"]))
# If no partitions, show entire disk as unallocated
if not partitions:
slice = series.append("", total_size)
slice.setColor(fs_colors["Unallocated"])
slice.setLabelVisible(False)
# Add to legend
legend_label = f"Entire Disk ({FileSystemUtils.get_readable_size(total_size)}, 100%)"
legend_items.append((legend_label, fs_colors["Unallocated"]))
except Exception as e:
logger.error(f"Error creating allocation chart: {e}")
# Add error slice
series.append("Error Loading Data", 1)
# Create chart
chart = QChart()
chart.addSeries(series)
chart.setTitle("")
chart.setAnimationOptions(QChart.SeriesAnimations)
chart.legend().setVisible(False) # Use custom legend instead
# Minimal margins for maximum chart size
chart.setMargins(QMargins(0, 0, 0, 0))
chart.setBackgroundVisible(False)
# Create chart view
chart_view = QChartView(chart)
chart_view.setRenderHint(QPainter.Antialiasing)
chart_view.setMinimumSize(350, 350)
chart_view.setStyleSheet("border: none; background: transparent;")
return chart_view, legend_items
def create_action(self, icon_path, text, callback):
action = QAction(QIcon(icon_path), text, self)
action.triggered.connect(callback)
return action
def get_grandparent_inode(self, parent_inode, start_offset):
"""Helper method to determine the grandparent inode"""
# Root directory (5 is typically root in NTFS) has no parent
if parent_inode == 5:
return None
try:
# Get directory entries for parent
parent_entries = self.image_handler.get_directory_contents(start_offset, parent_inode)
# Look for parent directory entry (..)
for entry in parent_entries:
if entry.get("name") == "..":
return entry.get("inode_number")
# If we can't find the proper parent, try filesystem-specific approach
# For NTFS, parent of non-root directories is often inode 5
return 5
except Exception as e:
logger.error(f"Error finding grandparent inode: {str(e)}")
return None
# ==================== SEARCH AND FILTER HANDLERS ====================
def on_listing_table_item_clicked(self, item):
"""Handle clicks on listing table items - navigate tree view in search mode."""
# Only handle navigation if we're in search mode
if not self._search_mode:
return
# Get the file data from the clicked item
row = item.row()
name_item = self.listing_table.item(row, 0) # Name column
if not name_item:
return
file_data = name_item.data(Qt.UserRole)
if not file_data:
return
# Get the path from the file data
file_path = file_data.get('path', '')
if not file_path:
return
# Navigate the tree view to show this file's location
self.navigate_tree_to_path(file_path, file_data)
def navigate_tree_to_path(self, path, file_data):
"""Navigate and expand the tree view to show the specified path."""
if not path or not self.tree_viewer:
return
# Split the path into components (e.g., "/folder1/folder2/file.txt" -> ["folder1", "folder2", "file.txt"])
# Remove leading/trailing slashes and split
path_parts = [p for p in path.split('/') if p]
if not path_parts:
return
# Start from the root - find the partition/volume first
root = self.tree_viewer.invisibleRootItem()
current_item = None
# Find the correct partition by matching the start_offset from file_data
start_offset = file_data.get('start_offset')
if start_offset is not None:
for i in range(root.childCount()):
child = root.child(i)
child_data = child.data(0, Qt.UserRole)
if child_data and child_data.get('start_offset') == start_offset:
current_item = child
current_item.setExpanded(True)
break
if not current_item:
return
# Now traverse the path, expanding each folder
for part_index, part_name in enumerate(path_parts):
found = False
# Expand current item to load its children
if not current_item.isExpanded():
current_item.setExpanded(True)
# Give Qt time to process the expansion and load children
QApplication.processEvents()
# Search through children for the next part
for i in range(current_item.childCount()):
child = current_item.child(i)
child_text = child.text(0)
if child_text == part_name:
current_item = child
found = True
# If this is not the last part, expand it
if part_index < len(path_parts) - 1:
current_item.setExpanded(True)
QApplication.processEvents()
break
if not found:
# Path component not found, stop navigation
break
# Select and highlight the final item
if current_item:
self.tree_viewer.setCurrentItem(current_item)
self.tree_viewer.scrollToItem(current_item)
# Set a special background color to highlight the search result
# Store the original background to restore later
if not hasattr(self, '_original_tree_item_background'):
self._original_tree_item_background = None
# Clear previous highlight
if hasattr(self, '_highlighted_tree_item') and self._highlighted_tree_item:
if self._original_tree_item_background:
self._highlighted_tree_item.setBackground(0, self._original_tree_item_background)
# Save current item and its background
self._highlighted_tree_item = current_item
self._original_tree_item_background = current_item.background(0)
# Set red highlight for the found item
from PySide6.QtGui import QBrush, QColor
current_item.setBackground(0, QBrush(QColor(255, 100, 100, 100))) # Semi-transparent red
def on_listing_search_text_changed(self):
"""Handle text changes in search bar - auto-clear results if empty."""
search_query = self.listing_search_bar.text().strip()
# Auto-clear results when user manually empties the search bar
if not search_query and self._search_mode:
self.switch_to_browse_mode()
def trigger_listing_search(self):
"""Trigger search when Enter is pressed."""
search_query = self.listing_search_bar.text().strip()
if not search_query:
# If empty, just return to browse mode
self.switch_to_browse_mode()
return
# Store the query
self._search_query = search_query
# Switch to search mode if not already
if not self._search_mode:
self.switch_to_search_mode()
# Perform the search
self.perform_search(search_query)
def _execute_search(self):
"""Execute the search after debounce delay."""
if self._search_query:
# Switch to search mode and perform search
self.switch_to_search_mode()
def clear_listing_search(self):
"""Clear the search bar and return to browse mode."""
self.listing_search_bar.clear() # This will trigger on_listing_search_text_changed
# Return to browse mode
if self._search_mode:
self.switch_to_browse_mode()
def switch_to_search_mode(self):
"""Switch from Browse mode to Search mode."""
if self._search_mode:
return # Already in search mode
# Save current browse state
self._last_browsed_state = {
'offset': self.current_offset,
'path': self.current_path,
'directory_data': self.current_selected_data
}
# Switch to search mode
self._search_mode = True
# Keep tree view enabled - user can still navigate while searching
# (removed: self.tree_viewer.setEnabled(False))
# Show Path column (critical for search results)
self.listing_table.setColumnHidden(8, False) # Path column
# Update status bar
statusbar = self.statusBar()
statusbar.showMessage(f"Searching for '{self._search_query}'...")
# Perform the search
self.perform_search(self._search_query)
def switch_to_browse_mode(self):
"""Switch from Search mode to Browse mode."""
if not self._search_mode:
return # Already in browse mode
# Switch to browse mode
self._search_mode = False
self._search_query = ""
# Clear any tree view highlights from search results
if hasattr(self, '_highlighted_tree_item') and self._highlighted_tree_item:
if hasattr(self, '_original_tree_item_background') and self._original_tree_item_background:
self._highlighted_tree_item.setBackground(0, self._original_tree_item_background)
self._highlighted_tree_item = None
self._original_tree_item_background = None
# Tree view stays enabled (removed: self.tree_viewer.setEnabled(True))
# Hide Path column in browse mode (tree shows location)
self.listing_table.setColumnHidden(8, True)
# Restore previous browse state
if self._last_browsed_state:
directory_data = self._last_browsed_state.get('directory_data')
path = self._last_browsed_state.get('path')
if directory_data and path:
try:
# Navigate the tree view back to this location
# This will also update the listing table via on_item_clicked
self._restore_tree_selection(path, directory_data)
except Exception as e:
self.statusBar().showMessage(f"Error restoring directory view: {str(e)}")
# Clear status bar
self.statusBar().clearMessage()
def _restore_tree_selection(self, path, directory_data):
"""Restore tree view selection to a previous location."""
if not path or not self.tree_viewer:
return
# Reuse the navigate_tree_to_path logic but without the red highlight
path_parts = [p for p in path.split('/') if p]
if not path_parts:
# Root path, select the partition
root = self.tree_viewer.invisibleRootItem()
start_offset = directory_data.get('start_offset')
if start_offset is not None:
for i in range(root.childCount()):
child = root.child(i)
child_data = child.data(0, Qt.UserRole)
if child_data and child_data.get('start_offset') == start_offset:
self.tree_viewer.setCurrentItem(child)
self.tree_viewer.scrollToItem(child)
# Manually trigger the item clicked event to update the listing table
self.on_item_clicked(child, 0)
break
return
# Full path restoration
root = self.tree_viewer.invisibleRootItem()
current_item = None
# Find the correct partition
start_offset = directory_data.get('start_offset')
if start_offset is not None:
for i in range(root.childCount()):
child = root.child(i)
child_data = child.data(0, Qt.UserRole)
if child_data and child_data.get('start_offset') == start_offset:
current_item = child
current_item.setExpanded(True)
break
if not current_item:
return
# Traverse the path
for part_index, part_name in enumerate(path_parts):
found = False
if not current_item.isExpanded():
current_item.setExpanded(True)
QApplication.processEvents()
for i in range(current_item.childCount()):
child = current_item.child(i)
if child.text(0) == part_name:
current_item = child
found = True
if part_index < len(path_parts) - 1:
current_item.setExpanded(True)
QApplication.processEvents()
break
if not found:
break
# Select the final item and trigger the click to update listing table
if current_item:
self.tree_viewer.setCurrentItem(current_item)
self.tree_viewer.scrollToItem(current_item)
# Manually trigger the item clicked event to update the listing table
self.on_item_clicked(current_item, 0)
def _wildcard_to_regex(self, pattern):
"""Convert wildcard pattern (*.pdf, name.*) to regex pattern."""
# Escape special regex characters except * and ?
pattern = re.escape(pattern)
# Replace escaped wildcards with regex equivalents
pattern = pattern.replace(r'\*', '.*') # * matches any characters
pattern = pattern.replace(r'\?', '.') # ? matches single character
return f"^{pattern}$" # Match entire string
def _matches_wildcard(self, filename, pattern):
"""Check if filename matches wildcard pattern."""
regex_pattern = self._wildcard_to_regex(pattern)
return re.match(regex_pattern, filename, re.IGNORECASE) is not None
def perform_search(self, search_query):
"""Execute file search with wildcard support."""
if not self.image_handler:
return
statusbar = self.statusBar()
statusbar.showMessage(f"Searching for '{search_query}'...")
try:
# Check if search query contains wildcards
has_wildcards = '*' in search_query or '?' in search_query
if has_wildcards:
# For wildcard searches, get all files and filter locally
files = self.image_handler.search_files(None)
# Filter by wildcard pattern
files = [f for f in files if self._matches_wildcard(f['name'], search_query)]
else:
# Regular substring search
files = self.image_handler.search_files(search_query)
# Clear and populate table
self.listing_table.setRowCount(0)
self.listing_table.setSortingEnabled(False)
# Show columns relevant for search results
self.listing_table.setColumnHidden(1, False) # Show Inode
self.listing_table.setColumnHidden(2, False) # Show Type (can be files or folders)
self.listing_table.setColumnHidden(4, False) # Show Created
self.listing_table.setColumnHidden(5, False) # Show Accessed
self.listing_table.setColumnHidden(6, False) # Show Modified
self.listing_table.setColumnHidden(7, False) # Show Changed
self.listing_table.setColumnHidden(8, False) # Show Path (critical for search)
self.listing_table.setColumnHidden(9, True) # Hide Info
# Populate with search results
for file in files:
self.insert_search_result_row(file)
self.listing_table.setSortingEnabled(True)
# Update status bar with result count
statusbar.showMessage(f"{len(files)} result(s) for '{search_query}'")
except Exception as e:
statusbar.showMessage(f"Search error: {str(e)}")
def insert_search_result_row(self, file_data):
"""Insert a search result into the listing table."""
row_position = self.listing_table.rowCount()
self.listing_table.insertRow(row_position)
# Get file icon based on type
file_name = file_data.get('name', '')
is_directory = file_data.get('is_directory', False)
if is_directory:
# Directory icon
icon_path = self.db_manager.get_icon_path('folder', 'folder')
else:
# File icon based on extension
extension = os.path.splitext(file_name)[1].lower()
# Remove the dot from extension for icon lookup (e.g., '.pdf' -> 'pdf')
ext_without_dot = extension[1:] if extension else 'txt'
icon_path = self.db_manager.get_icon_path('file', ext_without_dot)
# Create name item with icon
name_item = QTableWidgetItem(file_name)
name_item.setIcon(QIcon(icon_path))
name_item.setData(Qt.UserRole, file_data)
# Create other items
inode_item = QTableWidgetItem(str(file_data.get('inode_number', '')))
type_item = QTableWidgetItem("Folder" if is_directory else "File")
size_item = SizeTableWidgetItem(self.image_handler.get_readable_size(file_data.get('size', 0)))
size_item.setData(Qt.UserRole, file_data.get('size', 0))
created_item = QTableWidgetItem(file_data.get('created', ''))
accessed_item = QTableWidgetItem(file_data.get('accessed', ''))
modified_item = QTableWidgetItem(file_data.get('modified', ''))
changed_item = QTableWidgetItem(file_data.get('changed', ''))
path_item = QTableWidgetItem(file_data.get('path', ''))
# Set items in table
self.listing_table.setItem(row_position, 0, name_item)
self.listing_table.setItem(row_position, 1, inode_item)
self.listing_table.setItem(row_position, 2, type_item) # Type column
self.listing_table.setItem(row_position, 3, size_item)
self.listing_table.setItem(row_position, 4, created_item)
self.listing_table.setItem(row_position, 5, accessed_item)
self.listing_table.setItem(row_position, 6, modified_item)
self.listing_table.setItem(row_position, 7, changed_item)
self.listing_table.setItem(row_position, 8, path_item)
def apply_browse_filter(self, extensions):
"""Apply file type filter to current directory in browse mode."""
if not self.image_handler or self.current_offset is None:
return
try:
statusbar = self.statusBar()
statusbar.showMessage("Applying filter...")
if extensions is None:
# No filter - show all files in current directory (need to get current inode)
# For simplicity, refresh the current view
# This requires tracking current inode - for now, we'll just clear the message
statusbar.showMessage("Show all files in current directory")
# TODO: Implement proper directory refresh
else:
# Get all files from current directory and filter by extension
# This requires getting the current inode and filtering results
# For now, we'll use the list_files method from ImageHandler
files = self.image_handler.list_files(extensions)
# Clear and populate table with filtered results
self.listing_table.setRowCount(0)
self.listing_table.setSortingEnabled(False)
for file in files:
self.insert_search_result_row(file)
self.listing_table.setSortingEnabled(True)
statusbar.showMessage(f"{len(files)} file(s) matching selected types")
except Exception as e:
logger.error(f"Filter error: {str(e)}")
self.statusBar().showMessage(f"Filter error: {str(e)}")
def open_search_result_file(self, file_data):
"""Open a file from search results in the viewer tabs."""
# This is the same as double-clicking - open in viewer
# Use the existing file opening logic
self.load_file_content(file_data)
def show_file_in_directory(self, file_data):
"""Navigate to the file's directory in browse mode and select the file."""
try:
# Clear search and switch to browse mode
self.listing_search_bar.clear() # This triggers switch_to_browse_mode
# Get file's location details
start_offset = file_data.get('start_offset')
file_path = file_data.get('path', '')
file_inode = file_data.get('inode_number')
if start_offset is None or not file_path:
self.statusBar().showMessage("Cannot determine file location")
return
# Parse the path to get parent directory
# file_path format: "/path/to/file.txt"
path_parts = file_path.split('/')
if len(path_parts) < 2:
# File is in root
parent_inode = 5
self.current_path = "/"
else:
# Need to navigate to parent directory
# For simplicity, navigate to root for now
# TODO: Implement proper path-to-inode resolution for deep directories
parent_inode = 5
self.current_path = "/"
# Load the parent directory contents
entries = self.image_handler.get_directory_contents(start_offset, parent_inode)
self.current_offset = start_offset
self.populate_listing_table(entries, start_offset)
# Find and select the file in the table
for row in range(self.listing_table.rowCount()):
item = self.listing_table.item(row, 0)
if item:
item_data = item.data(Qt.UserRole)
if item_data and item_data.get('inode_number') == file_inode:
# Select this row
self.listing_table.selectRow(row)
# Scroll to make it visible
self.listing_table.scrollToItem(item)
break
# Update status bar
self.statusBar().showMessage(f"Showing {file_data.get('name', 'file')} in directory")
# TODO: Expand tree view to show this location
# This would require traversing the tree to find and expand the correct nodes
except Exception as e:
logger.error(f"Error showing file in directory: {str(e)}")
self.statusBar().showMessage(f"Error navigating to file location: {str(e)}")
# ==================== END SEARCH AND FILTER HANDLERS ====================
def on_listing_table_item_clicked(self, item):
"""Handle click events on the listing table."""
row = item.row()
# Get data from the name column (column 0)
data = self.listing_table.item(row, 0).data(Qt.UserRole)
if not data:
return
self.current_selected_data = data
statusbar = self.statusBar()
statusbar.showMessage("Loading content...")
try:
if data.get("type") == "volume":
# Handle volume/partition - navigate into its root directory
start_offset = data.get("start_offset", 0)
# Reset path to root of this volume
self.current_path = "/"
# Get root directory contents of the volume (inode 5 is typically root for NTFS)
entries = self.image_handler.get_directory_contents(start_offset, 5)
# Update directory up button - should be disabled since we're at volume root
self.update_directory_up_button()
# Populate listing table with volume contents
self.populate_listing_table(entries, start_offset)
# Add to navigation history
self._add_to_history(data)
statusbar.clearMessage()
elif data.get("type") == "directory":
inode_number = data.get("inode_number", 0)
# Find and select the corresponding item in the tree view if possible
self.select_tree_item_by_inode(inode_number, data["start_offset"])
# Update current path for directory navigation
if data.get("name") == "..":
# Go to parent directory
self.current_path = os.path.dirname(self.current_path)
if self.current_path == "":
self.current_path = "/"
elif data.get("inode_number") == 5: # Root directory
self.current_path = "/"
else:
# Navigate into directory
self.current_path = os.path.join(self.current_path, data.get("name", ""))
# Directories are processed synchronously
entries = self.image_handler.get_directory_contents(data["start_offset"], inode_number)
# Update directory up button state
self.update_directory_up_button()
self.populate_listing_table(entries, data["start_offset"])
# Add to navigation history
self._add_to_history(data)
statusbar.clearMessage()
else:
# Find and select the corresponding file in the tree view if possible
self.select_tree_item_by_inode(data.get("inode_number"), data["start_offset"])
# Files are processed in a background thread
inode_number = data.get("inode_number", 0)
self.file_worker = self.FileContentWorker(self.image_handler, inode_number, data["start_offset"])
self.file_worker.completed.connect(
lambda content, _: self.update_viewer_with_file_content(content, data))
self.file_worker.error.connect(
lambda msg: (self.log_error(msg), statusbar.clearMessage()))
self.file_worker.start()
except Exception as e:
self.log_error(f"Error processing listing table click: {str(e)}")
statusbar.clearMessage()
# Add a worker thread for exporting files and directories
class ExportWorker(QThread):
progress = Signal(int, int) # current, total
finished = Signal()
error = Signal(str)
status_update = Signal(str)
def __init__(self, image_handler, inode_number, offset, dest_dir, name, is_directory):
super().__init__()
self.image_handler = image_handler
self.inode_number = inode_number
self.offset = offset
self.dest_dir = dest_dir
self.name = name
self.is_directory = is_directory
self.total_items = 0
self.processed_items = 0
def run(self):
try:
if self.dest_dir:
if self.is_directory:
self._export_directory(self.inode_number, self.offset, self.dest_dir, self.name)
else:
self._export_file(self.inode_number, self.offset, self.dest_dir, self.name)
self.finished.emit()
except Exception as e:
self.error.emit(f"Export error: {str(e)}")
def _export_directory(self, inode_number, offset, dest_dir, name):
"""Export a directory with progress reporting."""
try:
# Create the directory in the destination
new_dest_dir = os.path.join(dest_dir, name)
os.makedirs(new_dest_dir, exist_ok=True)
# Get directory contents
entries = self.image_handler.get_directory_contents(offset, inode_number)
# Count total items for progress reporting
self._count_items_recursive(entries, offset)
# Export each entry
for entry in entries:
try:
self._export_item(
entry["inode_number"],
offset,
new_dest_dir,
entry["name"],
entry["is_directory"]
)
except Exception as e:
self.error.emit(f"Error exporting {entry['name']}: {str(e)}")
except Exception as e:
self.error.emit(f"Error exporting directory {name}: {str(e)}")
def _count_items_recursive(self, entries, offset):
"""Count total items in a directory and subdirectories."""
self.total_items += len(entries)
# Count items in subdirectories
for entry in entries:
if entry["is_directory"]:
sub_entries = self.image_handler.get_directory_contents(offset, entry["inode_number"])
self._count_items_recursive(sub_entries, offset)
def _export_item(self, inode_number, offset, dest_dir, name, is_directory):
"""Export a single item (file or directory)."""
self.status_update.emit(f"Exporting {name}")
if is_directory:
sub_dest_dir = os.path.join(dest_dir, name)
os.makedirs(sub_dest_dir, exist_ok=True)
# Get subdirectory contents
entries = self.image_handler.get_directory_contents(offset, inode_number)
# Export each entry in the subdirectory
for entry in entries:
self._export_item(
entry["inode_number"],
offset,
sub_dest_dir,
entry["name"],
entry["is_directory"]
)
else:
self._export_file(inode_number, offset, dest_dir, name)
# Update progress
self.processed_items += 1
self.progress.emit(self.processed_items, self.total_items)
def _export_file(self, inode_number, offset, dest_dir, name):
"""Export a single file with chunked processing."""
try:
file_content, _ = self.image_handler.get_file_content(inode_number, offset)
if file_content:
file_path = os.path.join(dest_dir, name)
with open(file_path, 'wb') as f:
f.write(file_content)
self.processed_items += 1
self.progress.emit(self.processed_items, self.total_items)
except Exception as e:
self.error.emit(f"Error exporting file {name}: {str(e)}")
================================================
FILE: modules/metadata_tab.py
================================================
import os
import datetime
from PySide6.QtWidgets import QTextEdit, QSizePolicy, QWidget, QVBoxLayout
import hashlib
from magic import Magic
import re
class MetadataViewer(QWidget):
def __init__(self, image_handler):
super(MetadataViewer, self).__init__()
self.image_handler = image_handler
self.init_ui()
def init_ui(self):
# Add the text edit to the layout
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self.metadata_text_edit = QTextEdit()
self.metadata_text_edit.setReadOnly(True)
self.metadata_text_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
layout.addWidget(self.metadata_text_edit)
def display_metadata(self, data):
# Check if this is a carved file with content already provided
is_carved = data.get('is_carved', False)
file_content = data.get('file_content')
if is_carved and file_content:
# Carved file - use provided content, no filesystem metadata available
metadata = None
else:
# Regular file - read from filesystem
inode_number = data.get('inode_number')
offset = data.get('start_offset')
file_content, metadata = self.image_handler.get_file_content(inode_number, offset)
if metadata is None:
self.metadata_text_edit.setHtml("No metadata available.")
return
def format_time(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"
# Handle timestamps - use filesystem metadata if available, otherwise use carved timestamp
if is_carved:
# For carved files, use the extracted/preserved timestamp
carved_timestamp = data.get('carved_timestamp', 'N/A')
created_time = 'N/A (carved file)'
modified_time = carved_timestamp if carved_timestamp != 'N/A' else 'N/A (carved file)'
accessed_time = 'N/A (carved file)'
changed_time = 'N/A (carved file)'
else:
# For regular files, use filesystem metadata
created_time = format_time(metadata.crtime) if hasattr(metadata, 'crtime') else 'N/A'
modified_time = format_time(metadata.mtime) if hasattr(metadata, 'mtime') else 'N/A'
accessed_time = format_time(metadata.atime) if hasattr(metadata, 'atime') else 'N/A'
changed_time = format_time(metadata.ctime) if hasattr(metadata, 'ctime') else 'N/A'
md5_hash = hashlib.md5(file_content).hexdigest() if file_content else "N/A"
sha256_hash = hashlib.sha256(file_content).hexdigest() if file_content else "N/A"
mime_type = Magic().from_buffer(file_content) if file_content else "N/A"
# Ensure size is an integer before passing to get_readable_size
if is_carved:
# For carved files, use size from data dict
size = data.get('size', 0)
size = self.image_handler.get_readable_size(size)
else:
# For regular files, use filesystem metadata
size = metadata.size if metadata.size else 'N/A'
if isinstance(size, str):
try:
size = int(size) # Convert size to int if it's a string
except ValueError:
size = 'N/A' # Keep as 'N/A' if conversion fails
else:
size = self.image_handler.get_readable_size(size) # Convert size to a readable format
# extended_metadata = f"Metadata"
extended_metadata = f"Metadata"
# Add carved file indicator if applicable
if is_carved:
extended_metadata += f"⚠ Carved File (recovered from unallocated space)
"
extended_metadata += f""
extended_metadata += f"| Name: | {data.get('name', 'N/A')} |
"
extended_metadata += f"| Type: | {data.get('type')} |
"
extended_metadata += f"| MIME Type: | {mime_type} |
"
extended_metadata += f"| Size: | {size} |
"
# Add disk offset for carved files
if is_carved:
offset_value = data.get('offset', 0)
extended_metadata += f"| Disk Offset: | {hex(offset_value)} ({offset_value} bytes) |
"
extended_metadata += f"| Modified: | {modified_time} |
"
extended_metadata += f"| Accessed: | {accessed_time} |
"
extended_metadata += f"| Created: | {created_time} |
"
extended_metadata += f"| Changed: | {changed_time} |
"
extended_metadata += f"| MD5: | {md5_hash} |
"
extended_metadata += f"| SHA-256: | {sha256_hash} |
"
extended_metadata += f"
"
extended_metadata += f"
"
extended_metadata += f"
"
# Skip istat for carved files (no inode available)
if not is_carved and os.name == 'nt':
istat_output = self.run_istat(data.get('start_offset'), data.get('inode_number'), self.image_handler.image_path)
extended_metadata += (
f"From The Sleuth Kit istat Tool")
extended_metadata += (f"")
extended_metadata += (f"
{istat_output}")
extended_metadata += (f"
")
self.metadata_text_edit.setHtml(extended_metadata)
def run_istat(self, offset, inode_number, image_path):
import subprocess
if image_path is None:
raise ValueError("Image path value is None!")
if inode_number is None:
raise ValueError("Inode number value is None!")
metadata_cmd = ["tools/sleuthkit-4.12.1-win32/bin/istat.exe"]
if offset is not None:
metadata_cmd.extend(["-o", str(offset)])
metadata_cmd.extend([image_path, str(inode_number)])
metadata_result = subprocess.run(
metadata_cmd,
capture_output=True,
text=True,
check=True
)
metadata_content = metadata_result.stdout
match = re.search(r"(init_size: \d+)", metadata_content)
if match:
end_index = match.end()
metadata_content = metadata_content[:end_index]
return metadata_content
def clear(self):
self.metadata_text_edit.clear()
================================================
FILE: modules/registry.py
================================================
import os
import tempfile
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem, QTextEdit, QToolBar, QLabel, \
QSplitter, QTableWidget, QTableWidgetItem, QComboBox, QSizePolicy, QPushButton, QMenu, QApplication, QHeaderView
from Registry import Registry
from Registry.Registry import RegistryValue, RegistryKey
class RegistryExtractor(QWidget):
def __init__(self, image_handler):
super().__init__()
self.image_handler = image_handler
self.hive_icon = QIcon("Icons/icons8-hive-48.png")
self.key_icon = QIcon("Icons/icons8-key-48_blue.png")
self.value_icon = QIcon("Icons/icons8-wasp-48.png")
self.init_ui()
def init_ui(self):
main_layout = QVBoxLayout()
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
self.setLayout(main_layout)
self.toolbar = QToolBar("Toolbar")
self.toolbar.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(self.toolbar)
self.icon_label = QLabel()
self.icon_label.setPixmap(QIcon("Icons/icons8-registry-editor-96.png").pixmap(48, 48))
self.toolbar.addWidget(self.icon_label)
self.label = QLabel("Registry Browser")
self.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.label)
spacer = QLabel()
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
self.toolbar.addWidget(spacer)
self.hiveSelector = QComboBox()
self.hiveSelector.addItems(["SOFTWARE", "SYSTEM", "SAM", "SECURITY", "DEFAULT", "COMPONENTS"])
self.toolbar.addWidget(self.hiveSelector)
self.loadHiveButton = QPushButton("Load")
self.loadHiveButton.clicked.connect(self.load_selected_hive)
self.toolbar.addWidget(self.loadHiveButton)
# Splitter setup
self.splitter = QSplitter(Qt.Horizontal)
main_layout.addWidget(self.splitter)
# Tree Widget Setup
self.treeWidget = QTreeWidget()
self.treeWidget.header().hide()
self.splitter.addWidget(self.treeWidget)
# Details Panel and Table Setup
self.detailsSplitter = QSplitter(Qt.Vertical)
self.splitter.addWidget(self.detailsSplitter)
# Metadata Panel Setup
self.metadataPanel = QTextEdit()
self.metadataPanel.setReadOnly(True)
self.detailsSplitter.addWidget(self.metadataPanel)
# Table Setup for displaying values
self.tableWidget = QTableWidget()
self.tableWidget.setEditTriggers(QTableWidget.NoEditTriggers)
self.tableWidget.setSelectionBehavior(QTableWidget.SelectRows)
self.tableWidget.verticalHeader().setVisible(False)
self.detailsSplitter.addWidget(self.tableWidget)
# Adjust proportions
self.splitter.setSizes([300, 700]) # Allocate space for the tree and details
self.detailsSplitter.setStretchFactor(0, 1) # Metadata panel
self.detailsSplitter.setStretchFactor(1, 1) # Table panel
# Connect the click event
self.treeWidget.itemClicked.connect(self.on_item_clicked)
def onCustomContextMenuRequested(self, position):
# Create the context menu
contextMenu = QMenu(self)
copyAction = contextMenu.addAction("Copy")
# Execute the menu and check which action was triggered
action = contextMenu.exec_(self.tableWidget.mapToGlobal(position))
if action == copyAction:
# Copy the selected cell's text to the clipboard
selectedIndexes = self.tableWidget.selectedIndexes()
if selectedIndexes:
selectedText = selectedIndexes[0].data() # Assuming single selection for simplicity
QApplication.clipboard().setText(selectedText)
def load_selected_hive(self):
try:
selectedHive = self.hiveSelector.currentText()
# Assuming get_partitions returns partitions where Windows is installed
partitions = self.image_handler.get_partitions()
if not partitions:
print("No partitions found.")
return
for partition in partitions:
start_offset = partition[2]
fs_type = self.image_handler.get_fs_type(start_offset)
fs_info = self.image_handler.get_fs_info(start_offset)
if fs_type == "NTFS":
# Modify to only load the selected hive
hive_data = self.image_handler.get_registry_hive(fs_info,
f"/Windows/System32/config/{selectedHive}")
if hive_data:
# Temporarily save the hive data to a file and load it
with tempfile.NamedTemporaryFile(delete=False) as temp_hive:
temp_hive.write(hive_data)
temp_hive_path = temp_hive.name
# Load the hive
with open(temp_hive_path, "rb") as hive_file:
reg = Registry.Registry(hive_file)
self.display_registry_hive(selectedHive, reg.root()) # Display the selected hive
os.remove(temp_hive_path)
except Exception as e:
print(f"An error occurred while loading the selected hive: {e}")
def display_registry_hive(self, hive_name, root_key):
self.treeWidget.clear() # Clear the tree before displaying a new hive
hive_item = QTreeWidgetItem(self.treeWidget, [hive_name])
hive_item.setIcon(0, self.hive_icon)
hive_item.setData(0, Qt.UserRole, root_key)
self.display_registry_keys(hive_item, root_key)
def display_registry_keys(self, parent_item, registry_key):
subkeys = registry_key.subkeys() # Call the method once and store the result
items = [QTreeWidgetItem(parent_item, [subkey.name()]) for subkey in subkeys] # Use list comprehension
for item, subkey in zip(items, subkeys):
item.setData(0, Qt.UserRole, subkey) # Store the key object for later retrieval
item.setIcon(0, self.key_icon)
self.display_registry_keys(item, subkey)
self.display_registry_values(item, subkey)
def display_registry_values(self, parent_key_item, registry_key):
values = registry_key.values() # Call the method once and store the result
items = [QTreeWidgetItem(parent_key_item, [value.name() or "(Default)"]) for value in
values] # Use list comprehension
for item, value in zip(items, values):
item.setData(0, Qt.UserRole, value) # Store the value object for later retrieval
item.setIcon(0, self.value_icon)
def display_metadata(self, registry_object):
metadata = {
"Name": registry_object.name(),
"Number of Subkeys": len(registry_object.subkeys()),
"Number of Values": len(registry_object.values()),
"Last Modified": registry_object.timestamp().strftime("%Y-%m-%d %H:%M:%S"),
}
# Start with an HTML structure for styling
details = ''
details += 'Metadata Information
'
for key, value in metadata.items():
details += f'{key}: {value}
'
details += ''
self.metadataPanel.setHtml(details)
def setup_table(self, values):
# Reset and set up table
self.tableWidget.clear()
self.tableWidget.setRowCount(len(values))
self.tableWidget.setColumnCount(3)
self.tableWidget.setHorizontalHeaderLabels(["Name", "Type", "Value"])
# Set initial widths to balance out based on common sizes
self.tableWidget.setColumnWidth(0, 150) # Name
self.tableWidget.setColumnWidth(1, 150) # Type
self.tableWidget.setColumnWidth(2, 450) # Value
# Set dynamic resizing behavior
header = self.tableWidget.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.Stretch) # Name column to stretch based on content
header.setSectionResizeMode(1, QHeaderView.ResizeToContents) # Type column adjusts to fit the content
header.setSectionResizeMode(2, QHeaderView.Stretch) # Value column stretches with window resize
# Populate table rows
for i, value in enumerate(values):
self.tableWidget.setItem(i, 0, QTableWidgetItem(value.name()))
self.tableWidget.setItem(i, 1, QTableWidgetItem(str(value.value_type_str())))
self.tableWidget.setItem(i, 2, QTableWidgetItem(str(value.value())))
def display_values_in_table(self, values):
self.setup_table(values)
def on_item_clicked(self, item, column):
registry_object = item.data(0, Qt.UserRole)
if isinstance(registry_object, RegistryKey):
self.display_metadata(registry_object)
self.display_values_in_table(registry_object.values())
elif isinstance(registry_object, RegistryValue):
self.setup_table([registry_object])
# clear the window
def clear(self):
self.treeWidget.clear()
self.metadataPanel.clear()
self.tableWidget.clear()
================================================
FILE: modules/text_tab.py
================================================
import base64
import html
import re
import sqlite3
import urllib
import urllib.parse
from enum import Enum
from functools import partial
import chardet
from PySide6.QtCore import Qt, QSize
from PySide6.QtGui import QAction, QIcon, QTextCursor, QTextCharFormat, QColor
from PySide6.QtWidgets import QWidget, QVBoxLayout, QTextEdit, QToolBar, QLineEdit, QSizePolicy, QComboBox, QLabel, \
QMessageBox, QToolTip, QToolButton, QMenu
class SearchDirection(Enum):
NEXT = 1
PREVIOUS = 2
class TextViewerManager:
PAGE_SIZE = 2000
def __init__(self):
self.file_content = b"" # Store raw byte content
self.text_content = "" # This will store the extracted strings
self.current_page = 0
self.encoding = 'utf-8'
self.last_search_str = ""
self.current_match_index = -1
self.page_changed_callback = None
self.matches = []
def get_total_pages(self):
return (len(self.text_content) - 1) // self.PAGE_SIZE + 1
@staticmethod
def detect_encoding(file_content_chunk):
detector = chardet.detect(file_content_chunk)
encoding = detector.get('encoding')
return encoding if encoding else 'utf-8'
def extract_strings_from_content(self):
encoding = self.detect_encoding(self.file_content[:1024]) # Detect encoding based on the first 1024 bytes
try:
text = self.file_content.decode(encoding)
except UnicodeDecodeError:
text = self.file_content.decode('ISO-8859-1') # Fallback to ISO-8859-1 if decoding fails
# Use regex to extract sequences of printable characters (length >= 4)
strings = re.findall(r"[ -~]{4,}", text)
# Join the strings with newlines to store them
self.text_content = "\n".join(strings)
def load_text_content(self, file_content):
self.file_content = file_content
self.extract_strings_from_content() # Extract printable strings
self.current_page = 0
def get_text_content_for_current_page(self):
start_idx = self.current_page * self.PAGE_SIZE
end_idx = (self.current_page + 1) * self.PAGE_SIZE
return self.text_content[start_idx:end_idx]
def change_page(self, delta):
new_page = self.current_page + delta
if 0 <= new_page * self.PAGE_SIZE < len(self.text_content):
self.current_page = new_page
if self.page_changed_callback:
self.page_changed_callback()
def jump_to_start(self):
self.current_page = 0
if self.page_changed_callback:
self.page_changed_callback()
def jump_to_end(self):
self.current_page = len(self.text_content) // self.PAGE_SIZE
if self.page_changed_callback:
self.page_changed_callback()
def search_for_string(self, search_str, direction=SearchDirection.NEXT):
if not search_str: # If search string is empty, do nothing
return
# Only find all occurrences of the search string if the search string has changed
if self.last_search_str != search_str:
self.matches = []
self.current_match_index = -1
# Find all occurrences of the search string in the text content
start_idx = 0
while start_idx < len(self.text_content):
idx = self.text_content.find(search_str, start_idx)
if idx == -1:
break
self.matches.append(idx)
start_idx = idx + len(search_str) # Update the start index to the end of the current match
self.last_search_str = search_str
# If no matches were found, return
if not self.matches:
return
# Update the current match index based on the search direction
if direction == SearchDirection.NEXT:
self.current_match_index = (self.current_match_index + 1) % len(self.matches)
else:
self.current_match_index = (self.current_match_index - 1) % len(self.matches)
# Update the current page to the page containing the current match
match_position = self.matches[self.current_match_index]
self.current_page = match_position // self.PAGE_SIZE
def clear_content(self):
self.file_content = b""
self.current_page = 0
self.last_search_str = ""
self.current_match_index = -1
self.matches = []
class TextViewer(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.manager = TextViewerManager()
self.manager.page_changed_callback = self.refresh_content
self.init_ui()
def init_ui(self):
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
self.setup_toolbar()
self.setup_text_edit()
self.setLayout(self.layout)
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"), "Jump to Start", self)
self.first_action.triggered.connect(self.manager.jump_to_start)
self.toolbar.addAction(self.first_action)
self.prev_action = QAction(QIcon("Icons/icons8-left-arrow-50.png"), "Previous Page", self)
self.prev_action.triggered.connect(lambda: self.manager.change_page(-1))
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 Page", self)
self.next_action.triggered.connect(lambda: self.manager.change_page(1))
self.toolbar.addAction(self.next_action)
self.last_action = QAction(QIcon("Icons/icons8-down-50.png"), "Jump to End", self)
self.last_action.triggered.connect(self.manager.jump_to_end)
self.toolbar.addAction(self.last_action)
# Add a small spacer
spacer = QWidget(self)
spacer.setFixedSize(20, 0)
self.toolbar.addWidget(spacer)
# Font size controls
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) # Set fixed 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 a spacer to push the search bar to the right
spacer = QWidget(self)
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
self.toolbar.addWidget(spacer)
# Search controls
self.search_input = QLineEdit(self)
self.search_input.setPlaceholderText("Search...")
self.search_input.setMaximumWidth(180) # Reduce width to save space
self.search_input.setFixedHeight(25) # Reduce height
self.search_input.setContentsMargins(5, 0, 5, 0) # Reduce margins
self.search_input.returnPressed.connect(self.search_next)
self.toolbar.addWidget(self.search_input)
self.layout.addWidget(self.toolbar)
def setup_text_edit(self):
self.text_edit = CustomTextEdit(self)
self.text_edit.setReadOnly(True)
self.layout.addWidget(self.text_edit)
def display_text_content(self, file_content):
self.manager.load_text_content(file_content)
self.refresh_content()
def clear_content(self):
self.text_edit.clear()
self.manager.clear_content()
def search_next(self):
# Call the search_for_string method with the updated match index
self.manager.search_for_string(self.search_input.text(), SearchDirection.NEXT)
# Update the highlighted text to highlight the current match
self.update_highlighted_text()
def update_highlighted_text(self):
if not self.manager.matches or not (0 <= self.manager.current_match_index < len(self.manager.matches)):
return
cursor = self.text_edit.textCursor()
cursor.clearSelection()
start_pos = self.manager.matches[self.manager.current_match_index] % self.manager.PAGE_SIZE
end_pos = start_pos + len(self.search_input.text())
highlight_format = QTextCharFormat()
highlight_format.setBackground(QColor("yellow"))
cursor.setPosition(start_pos, QTextCursor.MoveAnchor)
cursor.setPosition(end_pos, QTextCursor.KeepAnchor)
cursor.setCharFormat(highlight_format)
def update_font_size(self):
selected_size = int(self.font_size_combobox.currentText())
current_font = self.text_edit.font()
current_font.setPointSize(selected_size)
self.text_edit.setFont(current_font)
def go_to_page_by_entry(self):
try:
page_num = int(self.page_entry.text()) - 1
if 0 <= page_num < self.manager.get_total_pages():
self.manager.current_page = page_num
self.refresh_content()
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 refresh_content(self):
text_content = self.manager.get_text_content_for_current_page()
self.text_edit.setPlainText(text_content)
current_page = self.manager.current_page + 1 # Pages start from 1
total_pages = self.manager.get_total_pages()
self.page_entry.setText(str(current_page))
self.total_pages_label.setText(f" of {total_pages}")
class CustomTextEdit(QTextEdit):
def __init__(self, *args, **kwargs):
super(CustomTextEdit, self).__init__(*args, **kwargs)
self.setMouseTracking(True)
def contextMenuEvent(self, event):
menu = self.createStandardContextMenu()
menu.addSeparator()
# Define all decoding actions
decoding_actions = {
"Decode Base64": self.decodeBase64,
"Decode Hex": self.decodeHex,
"Decode URL": self.decodeURL,
"Decode HTML": self.decodeHTML,
"Decode Octal": self.decodeOctal,
"Decode Binary": self.decodeBinary,
}
for action_text, method in decoding_actions.items():
action = menu.addAction(action_text)
action.triggered.connect(partial(method))
menu.exec(event.globalPos())
def decodeBase64(self):
self.decodeSelectedText('base64')
def decodeHex(self):
self.decodeSelectedText('hex')
def decodeURL(self):
self.decodeSelectedText('url')
def decodeHTML(self):
self.decodeSelectedText('html')
def decodeOctal(self):
self.decodeSelectedText('octal')
def decodeBinary(self):
self.decodeSelectedText('binary')
def decodeSelectedText(self, encoding_type):
selected_text = self.textCursor().selectedText()
try:
if encoding_type == 'base64':
decoded_bytes = base64.b64decode(selected_text)
elif encoding_type == 'hex':
decoded_bytes = bytes.fromhex(selected_text)
elif encoding_type == 'url':
decoded_text = urllib.parse.unquote_plus(selected_text)
QToolTip.showText(self.mapToGlobal(self.cursorRect().topLeft()), decoded_text)
return
elif encoding_type == 'html':
decoded_text = html.unescape(selected_text)
QToolTip.showText(self.mapToGlobal(self.cursorRect().topLeft()), decoded_text)
return
elif encoding_type == 'octal':
decoded_text = ''.join(chr(int(octal, 8)) for octal in selected_text.split())
QToolTip.showText(self.mapToGlobal(self.cursorRect().topLeft()), decoded_text)
return
elif encoding_type == 'binary':
decoded_text = ''.join(chr(int(i, 2)) for i in selected_text.split())
QToolTip.showText(self.mapToGlobal(self.cursorRect().topLeft()), decoded_text)
return
if encoding_type in ['base64', 'hex']:
decoded_text = decoded_bytes.decode('utf-8')
QToolTip.showText(self.mapToGlobal(self.cursorRect().topLeft()), decoded_text)
except Exception as e:
QToolTip.showText(self.mapToGlobal(self.cursorRect().topLeft()), f"Invalid {encoding_type.upper()}")
def getDecodedText(self, selected_text):
# Attempt decoding in various formats
decoders = [
self.tryDecodeBinary,
self.tryDecodeOctal,
self.tryDecodeBase64,
self.tryDecodeHex,
self.tryDecodeURL,
self.tryDecodeHTML
]
for decoder in decoders:
decoded_text = decoder(selected_text)
if decoded_text:
return decoded_text
return None
def tryDecodeBase64(self, text):
try:
decoded_bytes = base64.b64decode(text, validate=True)
return decoded_bytes.decode('utf-8')
except Exception:
return None
def tryDecodeHex(self, text):
try:
decoded_bytes = bytes.fromhex(text)
return decoded_bytes.decode('utf-8')
except Exception:
return None
def tryDecodeURL(self, text):
try:
return urllib.parse.unquote_plus(text)
except Exception:
return None
def tryDecodeHTML(self, text):
try:
return html.unescape(text)
except Exception:
return None
def tryDecodeOctal(self, text):
try:
return ''.join(chr(int(octal, 8)) for octal in text.split())
except Exception:
return None
def tryDecodeBinary(self, text):
try:
decoded_text = ''.join(chr(int(i, 2)) for i in text.split())
return decoded_text
except Exception:
return None
def mouseMoveEvent(self, event):
super(CustomTextEdit, self).mouseMoveEvent(event)
# Check if there's selected text
selected_text = self.textCursor().selectedText()
if selected_text:
tooltip_text = self.getDecodedText(selected_text)
if tooltip_text:
QToolTip.showText(event.globalPos(), tooltip_text)
else:
QToolTip.hideText() # Hide any existing tooltip if there's no selection
================================================
FILE: modules/unified_application_manager.py
================================================
import os
from ctypes import cast, POINTER
from weakref import WeakValueDictionary
import mimetypes
import platform
import time
from PySide6.QtCore import Qt, QUrl, Slot, QSize, QTimer, QPoint, QBuffer, QByteArray, QIODevice
from PySide6.QtGui import QIcon, QPixmap, QImage, QAction, QPageLayout, QPainter, QColor, QPen, QTransform
from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput
from PySide6.QtMultimediaWidgets import QVideoWidget
from PySide6.QtPrintSupport import QPrinter, QPrintDialog
from PySide6.QtWidgets import (QToolBar, QMessageBox, QScrollArea, QLineEdit, QFileDialog, QApplication)
from PySide6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QSlider, QLabel, QHBoxLayout, QComboBox, \
QSpacerItem, QSizePolicy
from fitz import open as fitz_open, Matrix
if os.name == "nt": # Windows
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume
from comtypes import CLSCTX_ALL
class PyTsk3StreamDevice(QIODevice):
"""Custom QIODevice that streams data directly from pytsk3 file objects. """
def __init__(self, file_obj, file_size, parent=None):
"""Initialize the stream device. """
super().__init__(parent)
self.file_obj = file_obj
self.file_size = file_size
self.current_position = 0
self._is_closed = False # Track if device has been closed
def size(self):
"""Return the total size of the media file."""
return self.file_size
def isSequential(self):
"""Return False to indicate this device supports seeking."""
return False
def seek(self, pos):
"""Seek to a specific position in the file."""
if 0 <= pos <= self.file_size:
self.current_position = pos
# Call parent seek to update internal state
return super().seek(pos)
return False
def pos(self):
"""Return the current position in the file."""
return self.current_position
def atEnd(self):
"""Return True if at end of file."""
return self.current_position >= self.file_size
def readData(self, maxSize):
"""Read data from the pytsk3 file object."""
# Safety check: Don't read if device is closed
if self._is_closed:
return b''
if self.current_position >= self.file_size:
return b''
# Safety check: Make sure file_obj still exists
if not self.file_obj:
return b''
try:
# Calculate how much to read
bytes_to_read = min(maxSize, self.file_size - self.current_position)
# Read from pytsk3 file object
data = self.file_obj.read_random(self.current_position, bytes_to_read)
# Update position
self.current_position += len(data)
return data
except Exception as e:
# This is expected if the device was closed while reading
if not self._is_closed:
print(f"Error reading from pytsk3 file object: {e}")
return b''
def writeData(self, data):
"""Write data (not supported for read-only device)."""
return -1
def close(self):
"""Close the device and mark it as closed."""
self._is_closed = True
self.file_obj = None # Release reference to file object
super().close()
class UnifiedViewer(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.current_path = None
self.main_app = None
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
# Check if Icons directory exists and create it if needed
self.ensure_icons_directory()
# Create placeholder widget to show when nothing is loaded
self.placeholder = QLabel("No content loaded")
self.placeholder.setObjectName("placeholderLabel") # For stylesheet targeting
self.placeholder.setAlignment(Qt.AlignCenter)
self.layout.addWidget(self.placeholder)
# Initialize viewers as None for lazy loading
self._pdf_viewer = None
self._picture_viewer = None
self._audio_video_player = None
# Store media buffer for in-memory playback (keeps buffer alive during playback)
self._media_buffer = None
# Store stream device and file object for streaming playback from disk images
self._media_stream_device = None
self._media_file_obj = None
def ensure_icons_directory(self):
"""Check if Icons directory exists and create it if needed"""
icons_dir = "Icons"
if not os.path.exists(icons_dir):
try:
os.makedirs(icons_dir)
print(f"Created missing Icons directory: {icons_dir}")
# Create missing default icons
self.create_default_icon(os.path.join(icons_dir, "play.png"), (50, 50), (0, 255, 0))
self.create_default_icon(os.path.join(icons_dir, "pause.png"), (50, 50), (255, 165, 0))
self.create_default_icon(os.path.join(icons_dir, "stop.png"), (50, 50), (255, 0, 0))
self.create_default_icon(os.path.join(icons_dir, "volume.png"), (50, 50), (0, 0, 255))
self.create_default_icon(os.path.join(icons_dir, "mute.png"), (50, 50), (128, 128, 128))
except Exception as e:
print(f"Error creating Icons directory: {e}")
def create_default_icon(self, path, size, color):
"""Create a simple colored square icon at the specified path"""
try:
image = QImage(size[0], size[1], QImage.Format_ARGB32)
# Use literal transparent color instead of Qt.transparent
image.fill(QColor(0, 0, 0, 0))
painter = QPainter(image)
painter.setPen(QPen(QColor(*color)))
# Create a QColor with proper alpha channel
brush_color = QColor(*color)
brush_color.setAlpha(128) # Semi-transparent
painter.setBrush(brush_color)
if "play" in path:
# Draw play triangle
points = [
QPoint(10, 10),
QPoint(10, 40),
QPoint(40, 25)
]
painter.drawPolygon(points)
elif "pause" in path:
# Draw pause symbol
painter.drawRect(15, 10, 8, 30)
painter.drawRect(27, 10, 8, 30)
elif "stop" in path:
# Draw stop symbol
painter.drawRect(15, 15, 20, 20)
elif "volume" in path:
# Draw volume symbol
painter.drawRect(10, 20, 10, 10)
painter.drawArc(20, 10, 20, 30, -45 * 16, 90 * 16)
elif "mute" in path:
# Draw mute symbol
painter.drawRect(10, 20, 10, 10)
painter.drawLine(25, 15, 35, 35)
painter.drawLine(35, 15, 25, 35)
painter.end()
image.save(path)
except Exception as e:
print(f"Error creating default icon {path}: {e}")
def get_pdf_viewer(self):
"""Lazy initialization of PDF viewer"""
if self._pdf_viewer is None:
self._pdf_viewer = PDFViewer(self)
self._pdf_viewer.setVisible(False)
self.layout.addWidget(self._pdf_viewer)
return self._pdf_viewer
def get_picture_viewer(self):
"""Lazy initialization of picture viewer"""
if self._picture_viewer is None:
self._picture_viewer = PictureViewer(self)
self._picture_viewer.setVisible(False)
self.layout.addWidget(self._picture_viewer)
return self._picture_viewer
def get_audio_video_player(self):
"""Lazy initialization of audio/video player"""
if self._audio_video_player is None:
self._audio_video_player = AudioVideoPlayer(self)
self._audio_video_player.setVisible(False)
self.layout.addWidget(self._audio_video_player)
return self._audio_video_player
def load(self, content=None, mime_type=None, path=None, file_obj=None, file_size=None):
"""Load content into the appropriate viewer."""
# Clear any previous content
self.clear()
self.current_path = path
# Check if we have either content or file_obj
if not content and not file_obj:
self.placeholder.setVisible(True)
return
try:
# Process PDF files
if mime_type.startswith('application/pdf'):
viewer = self.get_pdf_viewer()
viewer.display(content)
viewer.setVisible(True)
self.placeholder.setVisible(False)
return True
# Process images
elif mime_type.startswith('image/'):
viewer = self.get_picture_viewer()
viewer.display(content)
viewer.setVisible(True)
self.placeholder.setVisible(False)
return True
# Process audio and video - use streaming if file_obj provided, otherwise QBuffer
elif mime_type.startswith(('audio/', 'video/')):
player = self.get_audio_video_player()
# Create a hint URL with the mime type to help the media backend
# identify the format correctly
hint_url = QUrl()
hint_url.setScheme("memory")
suffix = mimetypes.guess_extension(mime_type) or '.tmp'
hint_url.setPath(f"media{suffix}")
# OPTION 1: Stream from pytsk3 file object (for large files from disk images)
if file_obj is not None and file_size is not None:
print(f"Using streaming playback for {file_size} byte media file")
# Create custom stream device
self._media_stream_device = PyTsk3StreamDevice(file_obj, file_size, self)
# Open the stream device for reading
if not self._media_stream_device.open(QIODevice.ReadOnly):
print("Failed to open stream device for reading")
self.placeholder.setText("Error: Could not open stream device")
self.placeholder.setVisible(True)
return False
# Keep file_obj reference alive
self._media_file_obj = file_obj
# Set the media source from stream device
player.media_player.setSourceDevice(self._media_stream_device, hint_url)
# OPTION 2: Use QBuffer for in-memory playback (small files or pre-loaded content)
elif content is not None:
# Determine if we should use QBuffer based on size
file_size_mb = len(content) / (1024 * 1024)
print(f"Using in-memory playback for {file_size_mb:.2f} MB media file")
# Create QBuffer for in-memory playback
# QBuffer needs to stay alive during playback, so we store it as instance variable
self._media_buffer = QBuffer()
# Wrap content in QByteArray and set it to the buffer
byte_array = QByteArray(content)
self._media_buffer.setData(byte_array)
# Open buffer for reading
if not self._media_buffer.open(QIODevice.ReadOnly):
print("Failed to open media buffer for reading")
self.placeholder.setText("Error: Could not open media buffer")
self.placeholder.setVisible(True)
return False
# Set the media source from buffer
player.media_player.setSourceDevice(self._media_buffer, hint_url)
else:
self.placeholder.setText("Error: No content or stream source provided")
self.placeholder.setVisible(True)
return False
player.setVisible(True)
self.placeholder.setVisible(False)
# For audio files, configure for audio-only mode
if mime_type.startswith('audio/'):
try:
player.set_audio_only_mode(True)
except Exception as e:
print(f"Warning: Could not set audio-only mode: {e}")
return True
# Unsupported file type
else:
self.placeholder.setText(f"Unsupported file type: {mime_type}")
self.placeholder.setVisible(True)
return False
except Exception as e:
self.placeholder.setText(f"Error loading content: {str(e)}")
self.placeholder.setVisible(True)
return False
def clear(self):
"""Clear all viewers and free up resources."""
# Hide all viewers
if self._pdf_viewer:
self._pdf_viewer.clear()
self._pdf_viewer.setVisible(False)
if self._picture_viewer:
self._picture_viewer.clear()
self._picture_viewer.setVisible(False)
# Clean up media player
if self._audio_video_player:
try:
# Stop playback
self._audio_video_player.stop()
except Exception as e:
print(f"Error stopping media player: {e}")
self._audio_video_player.setVisible(False)
# Clean up media buffer
if self._media_buffer:
try:
if self._media_buffer.isOpen():
self._media_buffer.close()
self._media_buffer = None
except Exception as e:
print(f"Error closing media buffer: {e}")
# Clean up stream device - with safety delay
if self._media_stream_device:
# Store reference for delayed cleanup
old_stream_device = self._media_stream_device
old_file_obj = self._media_file_obj
# Clear references immediately
self._media_stream_device = None
self._media_file_obj = None
# Close the device after a short delay to let background threads finish
# This is non-blocking and happens asynchronously
def delayed_cleanup():
try:
if old_stream_device and old_stream_device.isOpen():
old_stream_device.close()
except Exception as e:
print(f"Error in delayed stream device cleanup: {e}")
# Schedule cleanup after 100ms (non-blocking)
QTimer.singleShot(100, delayed_cleanup)
else:
# No stream device, just clear file object
self._media_file_obj = None
# Show the placeholder
self.placeholder.setText("No content loaded")
self.placeholder.setVisible(True)
self.current_path = None
def display_application_content(self, file_content, full_file_path):
"""Wrapper for backward compatibility - converts file extension to MIME type."""
file_extension = os.path.splitext(full_file_path)[-1].lower()
mime_type = None
# Map common extensions to MIME types
if file_extension in ['.pdf']:
mime_type = 'application/pdf'
elif file_extension in ['.jpg', '.jpeg', '.png', '.bmp', '.gif']:
mime_type = f'image/{file_extension[1:]}'
elif file_extension in ['.mp3', '.wav', '.ogg', '.aac', '.m4a']:
mime_type = f'audio/{file_extension[1:]}'
elif file_extension in ['.mp4', '.mkv', '.flv', '.avi', '.mov', '.wmv']:
mime_type = 'video/mp4'
else:
# Default to binary data
mime_type = 'application/octet-stream'
# Call the new load method with the determined MIME type
return self.load(file_content, mime_type, full_file_path)
def closeEvent(self, event):
"""Handle proper cleanup when the widget is closed"""
# Make sure to stop any media playback
if self._audio_video_player:
try:
self._audio_video_player.stop()
except:
pass
# Clean up media buffer
if self._media_buffer:
try:
if self._media_buffer.isOpen():
self._media_buffer.close()
self._media_buffer = None
except:
pass
# Clean up stream device (immediate, not delayed)
if self._media_stream_device:
try:
if self._media_stream_device.isOpen():
self._media_stream_device.close()
self._media_stream_device = None
except:
pass
# Release file object
self._media_file_obj = None
# Accept the close event
super().closeEvent(event)
def __del__(self):
"""Ensure proper cleanup when the object is garbage collected"""
# Clean up media resources
try:
if self._media_buffer and self._media_buffer.isOpen():
self._media_buffer.close()
if self._media_stream_device and self._media_stream_device.isOpen():
self._media_stream_device.close()
self._media_file_obj = None
except:
pass # Ignore errors during cleanup in destructor
def shutdown(self):
"""Properly shut down all resources, especially media players.
Call this method before the application exits."""
try:
# Force close any open viewers first
if self._pdf_viewer:
self._pdf_viewer.clear()
if self._picture_viewer:
self._picture_viewer.clear()
# Explicit shutdown of audio/video player
if self._audio_video_player:
try:
# Stop media playback and remove references
self._audio_video_player.safe_stop()
QApplication.processEvents()
# Release reference
player = self._audio_video_player
self._audio_video_player = None
except Exception as e:
print(f"Error during audio/video player shutdown: {e}")
# Clean up media buffer
if self._media_buffer:
try:
if self._media_buffer.isOpen():
self._media_buffer.close()
self._media_buffer = None
except Exception as e:
print(f"Error closing media buffer during shutdown: {e}")
# Clean up stream device
if self._media_stream_device:
try:
if self._media_stream_device.isOpen():
self._media_stream_device.close()
self._media_stream_device = None
except Exception as e:
print(f"Error closing stream device during shutdown: {e}")
# Release file object reference
self._media_file_obj = None
# Process any pending events
QApplication.processEvents()
except Exception as e:
print(f"Error during UnifiedViewer shutdown: {e}")
class PictureViewer(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.original_pixmap = None # Store the original QPixmap
self.original_image_bytes = None # Store the original image bytes
self.initialize_ui()
def initialize_ui(self):
self.layout = QVBoxLayout()
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
self.layout.setAlignment(Qt.AlignCenter)
# Create a container for the toolbar and the application viewer
container_widget = QWidget(self)
container_layout = QVBoxLayout()
container_layout.setContentsMargins(0, 0, 0, 0) # Remove any margins
container_layout.setSpacing(0) # Remove spacing between toolbar and viewer
# Create and set up the toolbar
self.setup_toolbar()
# Add the toolbar to the container layout
container_layout.addWidget(self.toolbar)
self.image_label = QLabel(self)
self.image_label.setContentsMargins(0, 0, 0, 0)
self.image_label.setAlignment(Qt.AlignCenter)
self.scroll_area = QScrollArea(self)
self.scroll_area.setContentsMargins(0, 0, 0, 0)
self.scroll_area.setWidget(self.image_label)
self.scroll_area.setWidgetResizable(True)
container_layout.addWidget(self.scroll_area)
container_widget.setLayout(container_layout)
self.layout.addWidget(container_widget)
self.setLayout(self.layout)
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
# Disable right click
self.toolbar.setContextMenuPolicy(Qt.PreventContextMenu)
zoom_in_icon = QIcon("Icons/icons8-zoom-in-50.png")
zoom_out_icon = QIcon("Icons/icons8-zoom-out-50.png")
rotate_left_icon = QIcon("Icons/icons8-rotate-left-50.png")
rotate_right_icon = QIcon("Icons/icons8-rotate-right-50.png")
reset_icon = QIcon("Icons/icons8-no-rotation-50.png")
export_icon = QIcon("Icons/icons8-save-as-50.png")
zoom_in_action = QAction(zoom_in_icon, 'Zoom In', self)
zoom_out_action = QAction(zoom_out_icon, 'Zoom Out', self)
rotate_left_action = QAction(rotate_left_icon, 'Rotate Left', self)
rotate_right_action = QAction(rotate_right_icon, 'Rotate Right', self)
reset_action = QAction(reset_icon, 'Reset', self)
self.export_action = QAction(export_icon, 'Save Image', self)
zoom_in_action.triggered.connect(self.zoom_in)
zoom_out_action.triggered.connect(self.zoom_out)
rotate_left_action.triggered.connect(self.rotate_left)
rotate_right_action.triggered.connect(self.rotate_right)
reset_action.triggered.connect(self.reset)
self.export_action.triggered.connect(self.export_original_image)
# Add actions to the toolbar
self.toolbar.addAction(zoom_in_action)
self.toolbar.addAction(zoom_out_action)
self.toolbar.addAction(rotate_left_action)
self.toolbar.addAction(rotate_right_action)
self.toolbar.addAction(reset_action)
self.toolbar.addAction(self.export_action)
def display(self, content):
self.original_image_bytes = content # Save the original image bytes
# Convert byte data to QPixmap
qt_image = QImage.fromData(content)
pixmap = QPixmap.fromImage(qt_image)
self.original_pixmap = pixmap.copy() # Save the original pixmap
self.image_label.setPixmap(pixmap)
def clear(self):
self.image_label.clear()
def zoom_in(self):
self.image_label.setPixmap(self.image_label.pixmap().scaled(
self.image_label.width() * 1.2, self.image_label.height() * 1.2, Qt.KeepAspectRatio,
Qt.SmoothTransformation))
def zoom_out(self):
self.image_label.setPixmap(self.image_label.pixmap().scaled(
self.image_label.width() * 0.8, self.image_label.height() * 0.8, Qt.KeepAspectRatio,
Qt.SmoothTransformation))
def rotate_left(self):
transform = QTransform().rotate(-90)
pixmap = self.image_label.pixmap().transformed(transform)
self.image_label.setPixmap(pixmap)
def rotate_right(self):
transform = QTransform().rotate(90)
pixmap = self.image_label.pixmap().transformed(transform)
self.image_label.setPixmap(pixmap)
def reset(self):
if self.original_pixmap:
self.image_label.setPixmap(self.original_pixmap)
def export_original_image(self):
# Ensure that an image is currently loaded
if not self.original_image_bytes:
QMessageBox.warning(self, "Export Error", "No image is currently loaded.")
return
# Ask the user where to save the exported image
file_name, _ = QFileDialog.getSaveFileName(self, "Export Image", "",
"PNG (*.png);;JPEG (*.jpg *.jpeg);;All Files (*)")
# If a location is chosen, save the image
if file_name:
with open(file_name, 'wb') as f:
f.write(self.original_image_bytes)
QMessageBox.information(self, "Export Success", "Image exported successfully!")
class PDFViewer(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.pdf = None
self.current_page = 0
self.zoom_factor = 1.0
self.rotation_angle = 0
self.is_panning = False
self.pan_start_x = 0
self.pan_start_y = 0
self.pan_mode = False
# Optimize performance with page caching
self._page_cache = {} # Cache for rendered pages
self._cache_size = 5 # Maximum number of pages to cache
self.initialize_ui()
def initialize_ui(self):
# Set up the main layout
self.layout = QVBoxLayout()
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
self.layout.setAlignment(Qt.AlignCenter)
# Create a container for the toolbar and the application viewer
container_widget = QWidget(self)
container_layout = QVBoxLayout()
container_layout.setContentsMargins(0, 0, 0, 0)
container_layout.setSpacing(0)
# Create and set up the toolbar
self.setup_toolbar()
# Add the toolbar to the container layout
container_layout.addWidget(self.toolbar)
# Set up the PDF display area
self.setup_pdf_display_area()
container_layout.addWidget(self.scroll_area)
container_widget.setLayout(container_layout)
self.layout.addWidget(container_widget)
self.setLayout(self.layout)
self.update_navigation_states()
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
# 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.show_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.show_previous_page)
self.toolbar.addAction(self.prev_action)
# Page entry
self.page_entry = QLineEdit(self)
self.page_entry.setMaximumWidth(40)
self.page_entry.setFixedHeight(22) # Set fixed height
self.page_entry.setAlignment(Qt.AlignRight)
self.page_entry.returnPressed.connect(self.go_to_page)
self.toolbar.addWidget(self.page_entry)
# Total pages label
self.total_pages_label = QLabel(f"of {len(self.pdf)}" if self.pdf else "of 0")
self.total_pages_label.setFixedHeight(22) # Set fixed height
self.toolbar.addWidget(self.total_pages_label)
# Navigation buttons
self.next_action = QAction(QIcon("Icons/icons8-right-arrow-50.png"), "Next", self)
self.next_action.triggered.connect(self.show_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.show_last_page)
self.toolbar.addAction(self.last_action)
# Add small spacer
spacer = QWidget(self)
spacer.setFixedSize(20, 0)
self.toolbar.addWidget(spacer)
# Zoom actions
self.zoom_in_action = QAction(QIcon("Icons/icons8-zoom-in-50.png"), "Zoom In", self)
self.zoom_in_action.triggered.connect(self.zoom_in)
self.toolbar.addAction(self.zoom_in_action)
# QLineEdit for zoom percentage
self.zoom_percentage_entry = QLineEdit(self)
self.zoom_percentage_entry.setFixedWidth(60) # Set a fixed width for consistency
self.zoom_percentage_entry.setFixedHeight(22) # Set fixed height
self.zoom_percentage_entry.setAlignment(Qt.AlignRight)
self.zoom_percentage_entry.setPlaceholderText("100%") # Default zoom is 100%
self.zoom_percentage_entry.returnPressed.connect(self.set_zoom_from_entry)
self.toolbar.addWidget(self.zoom_percentage_entry)
self.zoom_out_action = QAction(QIcon("Icons/icons8-zoom-out-50.png"), "Zoom Out", self)
self.zoom_out_action.triggered.connect(self.zoom_out)
self.toolbar.addAction(self.zoom_out_action)
# Create a reset zoom button with its icon and add it to the toolbar
reset_zoom_icon = QIcon("Icons/icons8-zoom-to-actual-size-50.png")
self.reset_zoom_action = QAction(reset_zoom_icon, "Reset Zoom", self)
self.reset_zoom_action.triggered.connect(self.reset_zoom)
self.toolbar.addAction(self.reset_zoom_action)
# Add small spacer
spacer = QWidget(self)
spacer.setFixedSize(20, 0)
self.toolbar.addWidget(spacer)
# Fit in window
fit_window_icon = QIcon("Icons/icons8-enlarge-50.png")
self.fit_window_action = QAction(fit_window_icon, "Fit in Window", self)
self.fit_window_action.triggered.connect(self.fit_window)
self.toolbar.addAction(self.fit_window_action)
# Fit in width
fit_width_icon = QIcon("Icons/icons8-resize-horizontal-50.png")
self.fit_width_action = QAction(fit_width_icon, "Fit in Width", self)
self.fit_width_action.triggered.connect(self.fit_width)
self.toolbar.addAction(self.fit_width_action)
# Add small spacer
spacer = QWidget(self)
spacer.setFixedSize(20, 0)
self.toolbar.addWidget(spacer)
# Pan tool button
self.pan_tool_icon = QIcon("Icons/icons8-drag-50.png")
self.pan_tool_action = QAction(self.pan_tool_icon, "Pan Tool", self)
self.pan_tool_action.setCheckable(True)
self.pan_tool_action.toggled.connect(self.toggle_pan_mode)
self.toolbar.addAction(self.pan_tool_action)
# Add a spacer to push the following buttons to the right
spacer = QWidget(self)
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
self.toolbar.addWidget(spacer)
# Print button
self.print_icon = QIcon("Icons/icons8-print-50.png")
self.print_action = QAction(self.print_icon, "Print", self)
self.print_action.triggered.connect(self.print_pdf)
self.toolbar.addAction(self.print_action)
self.save_pdf_action = QAction(QIcon("Icons/icons8-save-as-50.png"), "Save PDF", self)
self.save_pdf_action.triggered.connect(self.save_pdf)
self.toolbar.addAction(self.save_pdf_action)
def setup_pdf_display_area(self):
self.page_label = QLabel(self)
self.page_label.setContentsMargins(0, 0, 0, 0)
self.page_label.setAlignment(Qt.AlignCenter)
self.scroll_area = QScrollArea(self)
self.scroll_area.setContentsMargins(0, 0, 0, 0)
self.scroll_area.setWidget(self.page_label)
self.scroll_area.setWidgetResizable(True)
def set_current_page(self, page_num):
"""Set the current page and update the view."""
if not self.pdf:
return
max_pages = len(self.pdf)
if 0 <= page_num < max_pages:
self.current_page = page_num
self.show_page(page_num)
def go_to_page(self):
"""Navigate to the page entered in the page entry field."""
try:
page_num = int(self.page_entry.text()) - 1 # Minus 1 because pages start from 0
self.set_current_page(page_num)
except ValueError:
QMessageBox.warning(self, "Invalid Page Number", "Please enter a valid page number.")
def update_navigation_states(self):
"""Update UI elements based on current PDF and page."""
if not self.pdf:
self.prev_action.setEnabled(False)
self.next_action.setEnabled(False)
self.first_action.setEnabled(False)
self.last_action.setEnabled(False)
self.total_pages_label.setText("of 0")
self.page_entry.setText("")
return
self.prev_action.setEnabled(self.current_page > 0)
self.next_action.setEnabled(self.current_page < len(self.pdf) - 1)
self.first_action.setEnabled(self.current_page > 0)
self.last_action.setEnabled(self.current_page < len(self.pdf) - 1)
self.total_pages_label.setText(f"of {len(self.pdf)}")
self.page_entry.setText(str(self.current_page + 1))
def show_previous_page(self):
"""Navigate to the previous page."""
self.set_current_page(self.current_page - 1)
self.update_navigation_states()
def show_next_page(self):
"""Navigate to the next page."""
self.set_current_page(self.current_page + 1)
self.update_navigation_states()
def show_page(self, page_num):
"""Display the specified page with caching for better performance."""
if not self.pdf:
return
try:
# Check if the page is in the cache
cache_key = (page_num, self.zoom_factor, self.rotation_angle)
if cache_key in self._page_cache:
# Use cached pixmap
self.page_label.setPixmap(self._page_cache[cache_key])
else:
# Render the page and cache it
page = self.pdf[page_num]
mat = Matrix(self.zoom_factor, self.zoom_factor).prerotate(self.rotation_angle)
image = page.get_pixmap(matrix=mat)
qt_image = QImage(image.samples, image.width, image.height, image.stride, QImage.Format_RGB888)
pixmap = QPixmap.fromImage(qt_image)
# Cache the pixmap
self._page_cache[cache_key] = pixmap
# Manage cache size
if len(self._page_cache) > self._cache_size:
# Remove oldest entry (first key)
oldest_key = next(iter(self._page_cache))
del self._page_cache[oldest_key]
self.page_label.setPixmap(pixmap)
self.update_navigation_states()
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to render page: {e}")
def display(self, content):
"""Load and display a PDF from content bytes."""
# Clear existing PDF and cache
self.clear()
if content:
try:
# Try to open PDF directly first
self.pdf = fitz_open(stream=content, filetype="pdf")
self.current_page = 0
self.zoom_factor = 1.0
self.rotation_angle = 0
self.show_page(self.current_page)
self.update_navigation_states()
except Exception as e:
# If direct open fails, try to clean up the PDF (common with carved files)
print(f"Initial PDF load failed: {e}, attempting cleanup...")
try:
cleaned_content = self.cleanup_pdf_content(content)
self.pdf = fitz_open(stream=cleaned_content, filetype="pdf")
self.current_page = 0
self.zoom_factor = 1.0
self.rotation_angle = 0
self.show_page(self.current_page)
self.update_navigation_states()
print("Successfully loaded PDF after cleanup")
except Exception as e2:
print(f"Failed to load PDF even after cleanup: {e2}")
else:
self.page_label.clear()
@staticmethod
def cleanup_pdf_content(content):
"""Clean up PDF content by removing trailing garbage after %%EOF.
Common issue with carved PDFs - extra bytes after the EOF marker
cause PyMuPDF to reject the file even though the PDF is valid.
"""
try:
# Find the last occurrence of %%EOF
eof_marker = b'%%EOF'
last_eof = content.rfind(eof_marker)
if last_eof != -1:
# Include the EOF marker plus a small buffer for trailing whitespace
# PDF spec allows whitespace/newlines after %%EOF, but not much else
end_position = last_eof + len(eof_marker)
# Look ahead a bit to include any trailing newlines (up to 10 bytes)
max_end = min(end_position + 10, len(content))
trailing_section = content[end_position:max_end]
# Count how many whitespace bytes follow EOF
whitespace_count = 0
for byte in trailing_section:
if byte in (0x0A, 0x0D, 0x20, 0x09): # \n, \r, space, tab
whitespace_count += 1
else:
break
# Truncate after EOF + whitespace
cleaned_content = content[:end_position + whitespace_count]
print(f"PDF cleanup: Truncated {len(content) - len(cleaned_content)} trailing bytes")
return cleaned_content
else:
# No EOF marker found, return original
print("PDF cleanup: No %%EOF marker found, returning original content")
return content
except Exception as e:
print(f"Error during PDF cleanup: {e}")
return content
def clear(self):
"""Close the PDF and clear all resources."""
if self.pdf:
self.pdf.close()
self.pdf = None
# Clear the cache
self._page_cache.clear()
self.page_label.clear()
self.update_navigation_states()
def show_first_page(self):
"""Navigate to the first page."""
self.set_current_page(0)
def show_last_page(self):
"""Navigate to the last page."""
if self.pdf:
self.set_current_page(len(self.pdf) - 1)
def zoom_in(self):
"""Increase zoom level."""
# Don't allow extreme zoom levels
if self.zoom_factor < 5.0:
self.zoom_factor *= 1.2
# Clear cache on zoom change
self._page_cache.clear()
self.show_page(self.current_page)
# Update zoom display
self.zoom_percentage_entry.setText(f"{int(self.zoom_factor * 100)}%")
def zoom_out(self):
"""Decrease zoom level."""
# Don't allow extreme zoom levels
if self.zoom_factor > 0.1:
self.zoom_factor *= 0.8
# Clear cache on zoom change
self._page_cache.clear()
self.show_page(self.current_page)
# Update zoom display
self.zoom_percentage_entry.setText(f"{int(self.zoom_factor * 100)}%")
def set_zoom_from_entry(self):
"""Set zoom level from the entry field."""
try:
# Extract the percentage from the QLineEdit
text = self.zoom_percentage_entry.text().strip('%')
percentage = float(text) / 100
if 0.1 <= percentage <= 5: # Enforce reasonable zoom limits
self.zoom_factor = percentage
# Clear cache on zoom change
self._page_cache.clear()
self.show_page(self.current_page)
else:
QMessageBox.warning(self, "Invalid Zoom", "Please enter a zoom percentage between 10% and 500%.")
# Reset the entry to the current zoom
self.zoom_percentage_entry.setText(f"{int(self.zoom_factor * 100)}%")
except ValueError:
QMessageBox.warning(self, "Invalid Zoom", "Please enter a valid zoom percentage.")
# Reset the entry to the current zoom
self.zoom_percentage_entry.setText(f"{int(self.zoom_factor * 100)}%")
def reset_zoom(self):
"""Reset zoom to original size."""
self.zoom_factor = 1.0
# Clear cache on zoom change
self._page_cache.clear()
self.show_page(self.current_page)
self.zoom_percentage_entry.setText("100%")
def fit_window(self):
"""Adjust zoom to fit the entire page in the window."""
if not self.pdf or self.current_page >= len(self.pdf):
return
page = self.pdf[self.current_page]
zoom_x = self.scroll_area.width() / page.rect.width
zoom_y = self.scroll_area.height() / page.rect.height
self.zoom_factor = min(zoom_x, zoom_y) * 0.95 # 95% to add a small margin
# Clear cache on zoom change
self._page_cache.clear()
self.show_page(self.current_page)
# Update zoom display
self.zoom_percentage_entry.setText(f"{int(self.zoom_factor * 100)}%")
def fit_width(self):
"""Adjust zoom to fit the page width in the window."""
if not self.pdf or self.current_page >= len(self.pdf):
return
page = self.pdf[self.current_page]
self.zoom_factor = self.scroll_area.width() / page.rect.width * 0.95 # 95% to add a small margin
# Clear cache on zoom change
self._page_cache.clear()
self.show_page(self.current_page)
# Update zoom display
self.zoom_percentage_entry.setText(f"{int(self.zoom_factor * 100)}%")
def rotate_left(self):
"""Rotate the page 90 degrees counterclockwise."""
self.rotation_angle -= 90
# Clear cache on rotation change
self._page_cache.clear()
self.show_page(self.current_page)
def rotate_right(self):
"""Rotate the page 90 degrees clockwise."""
self.rotation_angle += 90
# Clear cache on rotation change
self._page_cache.clear()
self.show_page(self.current_page)
def toggle_pan_mode(self, checked):
"""Enable or disable panning mode."""
self.pan_mode = checked
self.setCursor(Qt.OpenHandCursor if checked else Qt.ArrowCursor)
def mousePressEvent(self, event):
"""Handle mouse press events for panning."""
if event.button() == Qt.LeftButton and self.pan_mode:
self.is_panning = True
self.pan_start_x = event.x()
self.pan_start_y = event.y()
self.setCursor(Qt.ClosedHandCursor) # Change to closed hand cursor while panning
event.accept()
def mouseMoveEvent(self, event):
"""Handle mouse move events for panning."""
if self.is_panning and self.pan_mode:
# Calculate the distance moved
dx = event.x() - self.pan_start_x
dy = event.y() - self.pan_start_y
# Update scroll position
self.scroll_area.horizontalScrollBar().setValue(self.scroll_area.horizontalScrollBar().value() - dx)
self.scroll_area.verticalScrollBar().setValue(self.scroll_area.verticalScrollBar().value() - dy)
# Update the mouse position for the next move
self.pan_start_x = event.x()
self.pan_start_y = event.y()
event.accept()
def mouseReleaseEvent(self, event):
"""Handle mouse release events for panning."""
if event.button() == Qt.LeftButton and self.is_panning and self.pan_mode:
self.is_panning = False
self.setCursor(Qt.OpenHandCursor) # Change back to open hand cursor
event.accept()
def print_pdf(self):
"""Print the current PDF."""
if not self.pdf:
QMessageBox.warning(self, "No Document", "No document available to print.")
return
printer = QPrinter()
printer.setFullPage(True)
printer.setPageOrientation(QPageLayout.Portrait)
print_dialog = QPrintDialog(printer, self)
if print_dialog.exec_() == QPrintDialog.Accepted:
from PySide6.QtGui import QPainter
try:
painter = QPainter()
if not painter.begin(printer):
QMessageBox.critical(self, "Error", "Failed to initialize printer.")
return
num_pages = len(self.pdf)
for i in range(num_pages):
if i != 0: # start a new page after the first one
printer.newPage()
# Render the page at a higher resolution for printing
page = self.pdf[i]
image = page.get_pixmap(matrix=Matrix(2.0, 2.0)) # Higher resolution for print
qt_image = QImage(image.samples, image.width, image.height, image.stride, QImage.Format_RGB888)
pixmap = QPixmap.fromImage(qt_image)
# Scale to printer page
rect = painter.viewport()
size = pixmap.size()
size.scale(rect.size(), Qt.KeepAspectRatio)
painter.setViewport(rect.x(), rect.y(), size.width(), size.height())
painter.setWindow(pixmap.rect())
painter.drawPixmap(0, 0, pixmap)
painter.end()
QMessageBox.information(self, "Print Complete", "Document was sent to the printer.")
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to print document: {e}")
if painter.isActive():
painter.end()
def save_pdf(self):
"""Save the current PDF to a file."""
if not self.pdf:
QMessageBox.warning(self, "No Document", "No document available to save.")
return
options = QFileDialog.Options()
filePath, _ = QFileDialog.getSaveFileName(self, "Save PDF", "", "PDF Files (*.pdf);;All Files (*)",
options=options)
if not filePath:
return # user cancelled the dialog
if not filePath.endswith(".pdf"):
filePath += ".pdf"
try:
self.pdf.save(filePath) # save the PDF to the specified path
QMessageBox.information(self, "Success", "PDF saved successfully!")
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to save PDF: {e}")
class AudioVideoPlayer(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
# Initialize attributes before calling methods that use them
self._is_playing = False
self._current_volume = 50 # Default volume level
self._is_muted = False
self._previous_volume = 50 # Store previous volume when muting
self._audio_session = None
self._volume_interface = None
self._is_audio_only = False # Flag to track if we're playing audio-only content
self._shutting_down = False # Flag to indicate shutdown in progress
# Now initialize UI and connections
self.initialize_ui()
self.setup_connections()
self._setup_os_volume()
def initialize_ui(self):
# Main layout
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
# Create video widget
self.video_widget = QVideoWidget(self)
self.video_widget.setMinimumSize(QSize(400, 300))
# Create media player
self.media_player = QMediaPlayer(self)
self.audio_output = QAudioOutput(self)
self.media_player.setVideoOutput(self.video_widget)
self.media_player.setAudioOutput(self.audio_output)
# Create label to display when playing audio-only content
self.audio_label = QLabel("Playing Audio", self)
self.audio_label.setObjectName("audioOnlyLabel") # For stylesheet targeting
self.audio_label.setAlignment(Qt.AlignCenter)
self.audio_label.setVisible(False)
# Set default volume
self.audio_output.setVolume(self._current_volume / 100.0)
# Create controls
self.create_controls()
# Add widgets to layout
self.layout.addWidget(self.video_widget)
self.layout.addWidget(self.audio_label)
self.layout.addWidget(self.control_widget)
def set_audio_only_mode(self, is_audio_only=True):
"""Configure the player for audio-only content"""
self._is_audio_only = is_audio_only
self.video_widget.setVisible(not is_audio_only)
self.audio_label.setVisible(is_audio_only)
# Set the media player flags accordingly
try:
if hasattr(self.media_player, 'setOption'):
if is_audio_only:
# For audio-only content, set flags that optimize for audio playback
self.media_player.setOption("audio-only", "true")
self.media_player.setOption("skip-video", "true")
else:
# Reset flags for video content
self.media_player.setOption("audio-only", "false")
self.media_player.setOption("skip-video", "false")
except Exception as e:
print(f"Warning: Could not set audio-only mode options: {e}")
def handle_media_status_change(self, status):
"""Handle media status changes"""
# If this is an audio file and we see no video streams, switch to audio-only mode
try:
if status == QMediaPlayer.LoadedMedia:
# Check if we can detect if this is audio-only content
has_video = False
if hasattr(self.media_player, 'hasVideo'):
has_video = self.media_player.hasVideo()
# Set the appropriate mode
self.set_audio_only_mode(not has_video)
except Exception as e:
print(f"Warning: Error detecting audio/video mode: {e}")
def setup_connections(self):
# Media player signals (updated for newer API)
self.media_player.errorOccurred.connect(self.handle_error)
self.media_player.positionChanged.connect(self.update_position)
self.media_player.durationChanged.connect(self.update_duration)
self.media_player.playbackStateChanged.connect(self.update_play_state)
# Media status signals - if available in this version
if hasattr(self.media_player, 'mediaStatusChanged'):
self.media_player.mediaStatusChanged.connect(self.handle_media_status_change)
# Control signals
self.play_button.clicked.connect(self.toggle_play)
self.stop_button.clicked.connect(self.stop)
self.position_slider.sliderMoved.connect(self.set_position)
self.volume_button.clicked.connect(self.toggle_mute)
self.volume_slider.valueChanged.connect(self.set_volume)
def _setup_os_volume(self):
"""Set up OS-specific volume control (Windows only)"""
if platform.system() == "Windows":
try:
from comtypes import CLSCTX_ALL
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume
devices = AudioUtilities.GetSpeakers()
self._audio_session = AudioUtilities.GetAllSessions()
interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None)
self._volume_interface = cast(interface, POINTER(IAudioEndpointVolume))
except Exception as e:
print(f"Could not initialize Windows audio integration: {e}")
def set_os_volume(self, volume_level):
"""Set system volume (Windows only)"""
if self._volume_interface and platform.system() == "Windows":
try:
# Convert from 0-100 to 0.0-1.0 range
self._volume_interface.SetMasterVolumeLevelScalar(volume_level / 100.0, None)
except Exception as e:
print(f"Error setting system volume: {e}")
def toggle_play(self):
if self._is_playing:
self.media_player.pause()
else:
self.media_player.play()
def stop(self):
"""Stop playback and reset position"""
try:
if hasattr(self, 'media_player') and self.media_player:
self.media_player.stop()
self._is_playing = False
self.update_controls()
except Exception as e:
print(f"Error stopping media playback: {e}")
def update_play_state(self, state):
# Updated for newer API
self._is_playing = (state == QMediaPlayer.PlayingState)
self.update_controls()
def update_controls(self):
if self._is_playing:
# Try different pause icon paths
pause_icon_paths = [
"Icons/icons8-pause-50.png",
"Icons/pause.png",
"Icons/icons8-pause-button-50.png"
]
icon_set = False
for path in pause_icon_paths:
if os.path.exists(path):
self.play_button.setIcon(QIcon(path))
icon_set = True
break
if not icon_set:
self.play_button.setText("Pause")
else:
# Try different play icon paths
play_icon_paths = [
"Icons/icons8-play-50.png",
"Icons/play.png",
"Icons/icons8-circled-play-50.png"
]
icon_set = False
for path in play_icon_paths:
if os.path.exists(path):
self.play_button.setIcon(QIcon(path))
icon_set = True
break
if not icon_set:
self.play_button.setText("Play")
def set_position(self, position):
self.media_player.setPosition(position)
def update_position(self, position):
# Block signals to prevent slider feedback loops
self.position_slider.blockSignals(True)
self.position_slider.setValue(position)
self.position_slider.blockSignals(False)
# Update time label
self.current_time_label.setText(self.format_time(position))
def update_duration(self, duration):
self.position_slider.setRange(0, duration)
self.total_time_label.setText(self.format_time(duration))
def format_time(self, milliseconds):
seconds = milliseconds // 1000
minutes = seconds // 60
seconds %= 60
return f"{minutes:02d}:{seconds:02d}"
def toggle_mute(self):
self._is_muted = not self._is_muted
if self._is_muted:
self._previous_volume = self._current_volume
self.set_volume(0)
# Try different mute icon paths
mute_icon_paths = [
"Icons/icons8-mute-50.png",
"Icons/mute.png"
]
icon_set = False
for path in mute_icon_paths:
if os.path.exists(path):
self.volume_button.setIcon(QIcon(path))
icon_set = True
break
if not icon_set:
self.volume_button.setText("Mute")
else:
self.set_volume(self._previous_volume)
# Try different volume icon paths
volume_icon_paths = [
"Icons/icons8-audio-50.png",
"Icons/volume.png",
"Icons/audio.png"
]
icon_set = False
for path in volume_icon_paths:
if os.path.exists(path):
self.volume_button.setIcon(QIcon(path))
icon_set = True
break
if not icon_set:
self.volume_button.setText("Vol")
# Update system volume if enabled
self.set_os_volume(self._current_volume)
def set_volume(self, volume):
self._current_volume = volume
self.audio_output.setVolume(volume / 100.0)
# Update volume slider
self.volume_slider.blockSignals(True)
self.volume_slider.setValue(volume)
self.volume_slider.blockSignals(False)
# Update mute button icon based on volume
if volume == 0:
self._is_muted = True
# Try different mute icon paths
mute_icon_paths = [
"Icons/icons8-mute-50.png",
"Icons/mute.png"
]
icon_set = False
for path in mute_icon_paths:
if os.path.exists(path):
self.volume_button.setIcon(QIcon(path))
icon_set = True
break
if not icon_set:
self.volume_button.setText("Mute")
else:
self._is_muted = False
# Try different volume icon paths
volume_icon_paths = [
"Icons/icons8-audio-50.png",
"Icons/volume.png",
"Icons/audio.png"
]
icon_set = False
for path in volume_icon_paths:
if os.path.exists(path):
self.volume_button.setIcon(QIcon(path))
icon_set = True
break
if not icon_set:
self.volume_button.setText("Vol")
# Update system volume if enabled
self.set_os_volume(volume)
def handle_error(self, error, error_string):
if error != QMediaPlayer.NoError:
QMessageBox.warning(self, "Media Error", f"Error: {error_string}")
def closeEvent(self, event):
# Clean up resources
try:
if hasattr(self, 'media_player') and self.media_player:
self.media_player.stop()
except Exception as e:
print(f"Error stopping media player during close: {e}")
super().closeEvent(event)
def __del__(self):
# Clean up any lingering resources
try:
if not hasattr(self, '_shutting_down') or not self._shutting_down:
self.safe_stop()
except Exception:
# Silently ignore errors during destruction
pass
def create_controls(self):
# Control widget and layout
self.control_widget = QWidget(self)
self.control_layout = QHBoxLayout(self.control_widget)
self.control_layout.setContentsMargins(2, 2, 2, 2)
self.control_layout.setSpacing(2)
# Play/Pause button with fallback icon paths
self.play_button = QPushButton(self)
# Try different icon paths
play_icon_paths = [
"Icons/icons8-play-50.png",
"Icons/play.png",
"Icons/icons8-circled-play-50.png"
]
for path in play_icon_paths:
if os.path.exists(path):
self.play_button.setIcon(QIcon(path))
break
else:
# Fallback - create a text button
self.play_button.setText("Play")
self.play_button.setIconSize(QSize(16, 16))
self.play_button.setFixedHeight(22)
self.play_button.setFlat(True)
self.play_button.setToolTip("Play/Pause")
# Stop button with fallback icon paths
self.stop_button = QPushButton(self)
# Try different icon paths
stop_icon_paths = [
"Icons/icons8-stop-50.png",
"Icons/stop.png",
"Icons/icons8-stop-circled-50.png"
]
for path in stop_icon_paths:
if os.path.exists(path):
self.stop_button.setIcon(QIcon(path))
break
else:
# Fallback - create a text button
self.stop_button.setText("Stop")
self.stop_button.setIconSize(QSize(16, 16))
self.stop_button.setFixedHeight(22)
self.stop_button.setFlat(True)
self.stop_button.setToolTip("Stop")
# Position slider
self.position_slider = QSlider(Qt.Horizontal, self)
self.position_slider.setFixedHeight(22)
self.position_slider.setRange(0, 0) # Will be updated when media is loaded
self.position_slider.setToolTip("Position")
# Time labels
self.current_time_label = QLabel("00:00", self)
self.current_time_label.setFixedHeight(22)
self.current_time_label.setMinimumWidth(40)
self.total_time_label = QLabel("00:00", self)
self.total_time_label.setFixedHeight(22)
self.total_time_label.setMinimumWidth(40)
# Volume button with fallback icon paths
self.volume_button = QPushButton(self)
# Try different icon paths
volume_icon_paths = [
"Icons/icons8-audio-50.png",
"Icons/volume.png",
"Icons/audio.png"
]
for path in volume_icon_paths:
if os.path.exists(path):
self.volume_button.setIcon(QIcon(path))
break
else:
# Fallback - create a text button
self.volume_button.setText("Vol")
self.volume_button.setIconSize(QSize(16, 16))
self.volume_button.setFixedHeight(22)
self.volume_button.setFlat(True)
self.volume_button.setToolTip("Mute/Unmute")
self.volume_slider = QSlider(Qt.Horizontal, self)
self.volume_slider.setFixedHeight(22)
self.volume_slider.setRange(0, 100)
self.volume_slider.setValue(self._current_volume)
self.volume_slider.setMaximumWidth(80)
self.volume_slider.setToolTip("Volume")
# Add controls to layout
self.control_layout.addWidget(self.play_button)
self.control_layout.addWidget(self.stop_button)
self.control_layout.addWidget(self.current_time_label)
self.control_layout.addWidget(self.position_slider)
self.control_layout.addWidget(self.total_time_label)
self.control_layout.addWidget(self.volume_button)
self.control_layout.addWidget(self.volume_slider)
def safe_stop(self):
"""Safely stop playback even during shutdown."""
try:
if hasattr(self, 'media_player') and self.media_player:
# Set flag to indicate we're shutting down
self._shutting_down = True
# Stop playback
self.media_player.stop()
# Process events to ensure stop command is processed
QApplication.processEvents()
# Release audio output
if hasattr(self, 'audio_output') and self.audio_output:
# Remove it from the media player first
if hasattr(self.media_player, 'setAudioOutput'):
try:
self.media_player.setAudioOutput(None)
# Process events to ensure this is applied
QApplication.processEvents()
except Exception as e:
print(f"Error removing audio output: {e}")
# Set media to null/empty to release resources
if hasattr(self.media_player, 'setSource'):
try:
self.media_player.setSource(QUrl())
# Process events to ensure this is applied
QApplication.processEvents()
except Exception as e:
print(f"Error clearing media source: {e}")
# Wait a moment for resources to be released
time.sleep(0.1)
except Exception as e:
print(f"Error in safe_stop: {e}")
================================================
FILE: modules/verification.py
================================================
from PySide6.QtGui import QIcon, QFont
from PySide6.QtWidgets import (QWidget, QLabel, QVBoxLayout, QPushButton, QApplication, QProgressBar, QHBoxLayout,
QFileDialog, QTextEdit)
from PySide6.QtCore import QThread, Signal, Qt
class HashCalculationThread(QThread):
hashCalculated = Signal(dict) # Signal for hash results
progressUpdated = Signal(float) # Signal for progress updates (percentage 0-100)
def __init__(self, image_handler):
super().__init__()
self.image_handler = image_handler
self.isRunning = True
def run(self):
try:
# Pass a progress callback to update the progress bar
hash_results = self.image_handler.calculate_hashes(
progress_callback=self.update_progress
)
if self.isRunning: # Check if we're still running before emitting the signal
self.hashCalculated.emit(hash_results)
except Exception as e:
print(f"Error in hash calculation thread: {e}")
if self.isRunning:
self.hashCalculated.emit({}) # Empty dict indicates error
def update_progress(self, current, total):
"""Handle progress updates safely with large values."""
try:
if total > 0 and self.isRunning:
# Convert to float to avoid overflow and limit to 0-100 range
percentage = min(100.0, (float(current) / float(total)) * 100.0)
self.progressUpdated.emit(percentage)
except Exception as e:
print(f"Progress update error: {e}")
def stop(self):
"""Safely stop the thread."""
self.isRunning = False
class VerificationWidget(QWidget):
def __init__(self, image_handler, parent=None):
super().__init__(parent)
self.image_handler = image_handler
self.thread = None
self.setWindowTitle("Trace - Image Verification")
self.setWindowIcon(QIcon('Icons/logo.png'))
self.setGeometry(100, 100, 750, 400) # Adjust size for better layout
self._verified = False # Track verification status
layout = QVBoxLayout(self)
layout.setContentsMargins(20, 20, 20, 20)
self.software_info = QLabel("Trace - Forensic Analysis Tool", self)
self.software_info.setObjectName("softwareInfoLabel")
layout.addWidget(self.software_info)
self.subtitle = QLabel("Image Hash Verification", self)
self.subtitle.setObjectName("subtitleLabel")
layout.addWidget(self.subtitle)
self.hash_label = QTextEdit("Calculating hashes...")
self.hash_label.setReadOnly(True)
self.hash_label.setFont(QFont("Courier", 10))
self.hash_label.setStyleSheet("""
QTextEdit {
background-color: #f0f0f0;
border: 1px solid #ccc;
color: #333;
font-family: 'Courier';
}
QTextEdit::indicator:checked {
background: #b0b0b0;
}
""")
layout.addWidget(self.hash_label)
progress_bar_container = QHBoxLayout()
progress_bar_container.addStretch()
self.progress_bar = QProgressBar()
self.progress_bar.setMinimum(0)
self.progress_bar.setMaximum(100) # Set to 100 for percentage display
self.progress_bar.setFixedWidth(400)
self.progress_bar.setAlignment(Qt.AlignCenter)
self.progress_bar.setStyleSheet("""
QProgressBar {
border: 2px solid grey;
border-radius: 5px;
text-align: center;
}
QProgressBar::chunk {
background-color: #05B8CC;
width: 20px;
}
""")
progress_bar_container.addWidget(self.progress_bar)
progress_bar_container.addStretch()
layout.addLayout(progress_bar_container)
button_layout = QHBoxLayout()
self.save_button = QPushButton("Save to Text File", self)
self.save_button.setFixedWidth(150)
self.save_button.clicked.connect(self.save_hash)
self.save_button.setEnabled(False)
button_layout.addWidget(self.save_button)
self.copy_button = QPushButton("Copy", self)
self.copy_button.setFixedWidth(150)
self.copy_button.clicked.connect(self.copy_hash)
self.copy_button.setEnabled(False)
button_layout.addWidget(self.copy_button)
self.close_button = QPushButton("Close", self)
self.close_button.setFixedWidth(150)
self.close_button.clicked.connect(self.close)
button_layout.addWidget(self.close_button)
layout.addLayout(button_layout)
# Start hash calculation with a slight delay to allow the UI to initialize
QApplication.processEvents()
self.start_hash_calculation()
def closeEvent(self, event):
"""Override closeEvent to properly clean up resources."""
if self.thread and self.thread.isRunning():
self.thread.stop() # Tell thread to stop processing
self.thread.wait(1000) # Wait up to 1 second
# If thread is still running, terminate it
if self.thread.isRunning():
self.thread.terminate()
self.thread.wait()
super().closeEvent(event)
def save_hash(self):
file_name, _ = QFileDialog.getSaveFileName(self, "Save Hash", "", "Text Files (*.txt)")
if file_name:
with open(file_name, 'w') as file:
file.write(self.hash_label.toPlainText())
def start_hash_calculation(self):
# Clean up any previous thread
if self.thread and self.thread.isRunning():
self.thread.stop()
self.thread.wait()
self.thread = HashCalculationThread(self.image_handler)
self.thread.hashCalculated.connect(self.on_hash_calculated)
self.thread.progressUpdated.connect(self.update_progress)
self.thread.start()
def update_progress(self, percentage):
"""Update progress bar with the given percentage."""
try:
self.progress_bar.setValue(int(percentage))
QApplication.processEvents() # Keep UI responsive
except Exception as e:
print(f"Error updating progress bar: {e}")
def on_hash_calculated(self, hash_results):
"""Process hash results and update UI."""
try:
# Set the progress bar to 100% complete
self.progress_bar.setValue(100)
if hash_results and 'computed_md5' in hash_results:
verification_results = []
computed_md5 = hash_results.get('computed_md5')
computed_sha1 = hash_results.get('computed_sha1')
computed_sha256 = hash_results.get('computed_sha256')
# Check if the loaded image file is of E01 format
if self.image_handler and self.image_handler.get_image_type() == "ewf":
stored_md5 = hash_results.get('stored_md5')
stored_sha1 = hash_results.get('stored_sha1')
# Compare the computed MD5 and SHA1 hashes with the stored hashes
md5_result = "Match" if computed_md5 == stored_md5 else "Mismatch"
sha1_result = "Match" if computed_sha1 == stored_sha1 else "Mismatch"
# Set verification status
self._verified = md5_result == "Match" or sha1_result == "Match"
verification_results.append(f"Stored MD5: {stored_md5 or 'N/A'}")
verification_results.append(f"Computed MD5: {computed_md5}")
verification_results.append(
f"MD5 Verify result: {md5_result}
") # New line after MD5 verification result
verification_results.append(f"Stored SHA1: {stored_sha1 or 'N/A'}")
verification_results.append(f"Computed SHA1: {computed_sha1}")
verification_results.append(
f"SHA1 Verify result: {sha1_result}
") # New line after SHA1 verification result
else: # For other image types, only display computed hashes
verification_results.append(f"Computed MD5: {computed_md5}")
verification_results.append(f"Computed SHA1: {computed_sha1}")
# Display computed SHA256 hash for all image types
verification_results.append(f"Computed SHA256: {computed_sha256}")
# Convert size from bytes to megabytes
size_bytes = hash_results.get('size')
size_mb = size_bytes / (1024 * 1024)
hash_info = "
".join(verification_results)
hash_info += f"
Size: {size_bytes} bytes ({size_mb:.2f} MB)
Path: {hash_results.get('path')}"
self.hash_label.setHtml(hash_info)
self.save_button.setEnabled(True)
self.copy_button.setEnabled(True)
else:
self.hash_label.setText("Error calculating hashes. Please ensure the image is accessible.")
except Exception as e:
print(f"Error processing hash results: {e}")
self.hash_label.setText(f"Error processing results: {str(e)}")
def copy_hash(self):
clipboard = QApplication.clipboard()
clipboard.setText(self.hash_label.toPlainText())
@property
def is_verified(self):
# Return the verification status property
return self._verified
================================================
FILE: modules/veriphone_api.py
================================================
import requests
from PySide6.QtCore import Qt
from PySide6.QtGui import QPixmap, QIcon
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QPushButton, QTextBrowser, QLineEdit, QLabel, QToolBar,
QSizePolicy, QMessageBox)
class VeriphoneWidget(QWidget):
def __init__(self):
super().__init__()
self.api_key = None
self.init_ui()
def init_ui(self):
self.layout = QVBoxLayout(self)
self.layout.setSpacing(0)
self.layout.setContentsMargins(0, 0, 0, 0)
# set widget size but make it resizable
self.setFixedSize(600, 400)
# set window title
self.setWindowTitle("Veriphone Phone Number Verification")
# add icon to the window
self.setWindowIcon(QIcon('Icons/logo.png'))
# Toolbar setup
self.toolbar = QToolBar("Veriphone Toolbar", self)
self.toolbar.setContentsMargins(0, 0, 0, 0)
self.toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
self.layout.addWidget(self.toolbar)
# Phone input field
self.phone_input = QLineEdit(self)
# size of the input field
self.phone_input.setFixedSize(300, 30)
self.phone_input.setPlaceholderText("Enter phone number with country code")
self.toolbar.addWidget(self.phone_input)
# Connect returnPressed signal to verify_phone_number method
self.phone_input.returnPressed.connect(self.verify_phone_number)
# spacer
spacer = QWidget(self)
spacer.setFixedSize(10, 10)
self.toolbar.addWidget(spacer)
# Verify button in toolbar
verify_button = QPushButton("Verify", self)
verify_button.clicked.connect(self.verify_phone_number)
self.toolbar.addWidget(verify_button)
# Spacer widget to push the logo to the far right
spacer = QWidget(self)
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
self.toolbar.addWidget(spacer)
# Logo on the far right
self.logo_label = QLabel(self)
self.logo_pixmap = QPixmap("Icons/logo_veriphone.png") # Make sure the path is correct
self.logo_label.setPixmap(self.logo_pixmap.scaled(120, 70, Qt.KeepAspectRatio,
Qt.SmoothTransformation)) # Adjust 100x50 to your desired size
self.toolbar.addWidget(self.logo_label)
# Text browser for showing the results
self.info_text_edit = QTextBrowser(self)
self.info_text_edit.setReadOnly(True)
self.layout.addWidget(self.info_text_edit)
def set_api_key(self, key):
self.api_key = key
def use_api_key(self):
if not self.api_key:
raise ValueError("API key not set")
def verify_phone_number(self):
if not self.api_key:
QMessageBox.warning(self, "API Key Not Set",
"Please set the API key in the Options menu before verifying a phone number.")
return
phone_number = self.phone_input.text()
if phone_number:
self.update_veriphone_info(phone_number)
else:
QMessageBox.warning(self, "Input Error", "Please enter a phone number to verify.")
def update_veriphone_info(self, phone_number):
data = self.verify_phone_with_veriphone(phone_number)
if data.get('status') == 'success':
info_text = self.format_data_as_html(data)
self.info_text_edit.setHtml(info_text)
else:
self.info_text_edit.setText("Failed to fetch data or phone number is invalid.")
def verify_phone_with_veriphone(self, phone_number):
url = f"https://api.veriphone.io/v2/verify?phone={phone_number}&key={self.api_key}"
response = requests.get(url)
if response.status_code == 200:
return response.json()
else:
return {"status": "error", "message": "Failed to verify phone number."}
def format_data_as_html(self, data):
# Additional fields from the Veriphone API
phone_region = data.get('phone_region', 'N/A')
country = data.get('country', 'N/A')
country_code = data.get('country_code', 'N/A')
country_prefix = data.get('country_prefix', 'N/A')
international_number = data.get('international_number', 'N/A')
local_number = data.get('local_number', 'N/A')
e164 = data.get('e164', 'N/A')
carrier = data.get('carrier', 'N/A')
html_content = f"""
Veriphone Information
Phone Number: {data.get('phone', 'N/A')}
Valid: {data.get('phone_valid', 'N/A')}
Carrier: {carrier}
Type: {data.get('phone_type', 'N/A')}
Region: {phone_region}
Country: {country}
Country Code: {country_code}
Country Prefix: {country_prefix}
International Number: {international_number}
Local Number: {local_number}
E164 Format: {e164}
"""
return html_content
================================================
FILE: modules/virus_total_tab.py
================================================
import io
import zipfile
from datetime import date
from time import time
from PySide6.QtCore import Qt
from PySide6.QtGui import QAction, QIcon
from PySide6.QtSvgWidgets import QSvgWidget
from PySide6.QtWidgets import QWidget, QVBoxLayout, QToolBar, QWidgetAction, QSizePolicy, QTextBrowser, QPushButton, \
QHBoxLayout, QMessageBox
from requests import post as requests_post
from requests.exceptions import RequestException
class VirusTotal(QWidget):
def __init__(self):
super().__init__()
self.last_request_time = 0
self.requests_made_last_minute = 0
self.daily_requests_made = 0
self.current_date = date.today()
self.api_key = None
self.current_file_hash = None
self.current_file_content = None
self.current_file_name = None
self.init_ui()
def init_ui(self):
self.layout = QVBoxLayout(self)
self.layout.setSpacing(0)
self.layout.setContentsMargins(0, 0, 0, 0)
# First toolbar for the VirusTotal logo
self.logo_toolbar = QToolBar(self)
self.logo_toolbar.setContentsMargins(0, 0, 0, 0) # Add some margins to the toolbar for better aesthetics
self.setup_logo_toolbar()
self.layout.addWidget(self.logo_toolbar)
self.action_toolbar = QToolBar(self)
self.action_toolbar.setContentsMargins(0, 0, 0, 0)
self.setup_action_toolbar()
self.layout.addWidget(self.action_toolbar)
self.action_toolbar.setVisible(False) # Hide this toolbar initially
buttonLayout = QHBoxLayout()
# align the button vertically and horizontally
buttonLayout.setAlignment(Qt.AlignCenter) # Align the buttons to the center
self.pass_hash_button = QPushButton("Pass Hash")
self.pass_hash_button.clicked.connect(self.pass_hash)
self.pass_hash_button.setFixedSize(120, 40) # Set fixed size for a modern look
buttonLayout.addWidget(self.pass_hash_button)
# Upload File Button
self.upload_file_button = QPushButton("Upload File")
self.upload_file_button.clicked.connect(self.upload_file)
self.upload_file_button.setFixedSize(120, 40) # Set fixed size for a modern look
buttonLayout.addWidget(self.upload_file_button)
self.layout.addLayout(buttonLayout)
self.info_text_edit = QTextBrowser(self)
self.info_text_edit.setReadOnly(True)
self.info_text_edit.setVisible(False)
self.layout.addWidget(self.info_text_edit)
def set_api_key(self, key):
self.api_key = key
def use_api_key(self):
if not self.api_key:
raise ValueError("API key not set")
def spacer(self, policy1, policy2):
spacer = QWidget(self)
spacer.setSizePolicy(policy1, policy2)
return spacer
def setup_logo_toolbar(self):
self.logo_toolbar.addWidget(self.spacer(QSizePolicy.Expanding, QSizePolicy.Preferred))
self.virus_total_logo = QSvgWidget("Icons/VirusTotal_logo.svg")
self.virus_total_logo.setFixedSize(141, 27)
logo_action = QWidgetAction(self)
logo_action.setDefaultWidget(self.virus_total_logo)
self.logo_toolbar.addAction(logo_action)
self.virus_total_logo.mousePressEvent = self.virus_total_website
self.virus_total_logo.setCursor(Qt.PointingHandCursor)
def setup_action_toolbar(self):
self.view_in_browser_action = QAction(QIcon('Icons/apps/internet-web-browser.svg'), "View in Browser",
self)
self.view_in_browser_action.triggered.connect(self.view_in_browser)
self.action_toolbar.addAction(self.view_in_browser_action)
self.view_in_browser_action.setVisible(True)
self.back_action = QAction(QIcon('Icons/icons8-left-arrow-50.png'), "Back", self)
self.back_action.triggered.connect(self.reset_ui)
self.action_toolbar.addAction(self.back_action)
self.action_toolbar.addWidget(self.spacer(QSizePolicy.Expanding, QSizePolicy.Preferred))
self.virus_total_logo = QSvgWidget("Icons/VirusTotal_logo.svg")
self.virus_total_logo.setFixedSize(141, 27)
logo_action = QWidgetAction(self)
logo_action.setDefaultWidget(self.virus_total_logo)
self.action_toolbar.addAction(logo_action)
self.virus_total_logo.mousePressEvent = self.virus_total_website
self.virus_total_logo.setCursor(Qt.PointingHandCursor)
def virus_total_website(self, event):
import webbrowser
webbrowser.open("https://www.virustotal.com")
def reset_ui(self):
self.info_text_edit.setVisible(False)
self.pass_hash_button.setVisible(True)
self.upload_file_button.setVisible(True)
self.action_toolbar.setVisible(False) # Hide action toolbar
def set_file_hash(self, file_hash):
self.current_file_hash = file_hash
# set file content to expect file content as bytes and name as string
def set_file_content(self, file_content, file_name="unnamed_file"):
"""Sets the current file content and assigns a default name if none is provided."""
self.current_file_content = file_content
if not file_name:
self.current_file_name = "unnamed_file"
else:
self.current_file_name = file_name
def upload_file(self):
"""Prepares the file content and name for upload."""
if not self.api_key:
QMessageBox.warning(self, "API Key Not Set",
"Please set the API key in the Options menu before uploading a file.")
return
if self.current_file_content and self.current_file_name:
# Assuming current_file_content is the content of the file to upload,
# and current_file_name is the name of the file.
self.upload_file_to_virustotal(self.current_file_content, self.current_file_name)
else:
self.info_text_edit.setText("No file content or name provided.")
self.info_text_edit.setVisible(True)
def zip_file_in_memory(self, content: bytes, file_name: str):
"""Creates a zip archive in memory containing the given file."""
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'a', zipfile.ZIP_DEFLATED, False) as zip_file:
zip_file.writestr(file_name, content)
zip_buffer.seek(0)
return zip_buffer
def upload_file_to_virustotal(self, file_content, file_name):
"""Uploads a zipped file to VirusTotal."""
# Here, file_content should be the content of the file to upload,
# and file_name should be the name of the file inside the zip.
zip_buffer = self.zip_file_in_memory(file_content, file_name)
url = "https://www.virustotal.com/api/v3/files"
headers = {
"x-apikey": self.api_key
}
files = {'file': (file_name + '.zip', zip_buffer.getvalue())}
response = requests_post(url, headers=headers, files=files)
if response.status_code == 200:
self.process_vt_response(response.json())
else:
print("Failed to upload file to VirusTotal:", response.text)
def process_vt_response(self, response):
data = response.get('data', {})
file_id = data.get('id', 'N/A')
file_hash = data.get('attributes', {}).get('sha256', 'N/A')
upload_date = data.get('attributes', {}).get('date', 'N/A')
self.current_file_hash = file_hash
self.update_virustotal_info()
self.logo_toolbar.setVisible(False)
self.action_toolbar.setVisible(True)
self.view_in_browser_action.setVisible(False)
def pass_hash(self):
if not self.api_key:
QMessageBox.warning(self, "API Key Not Set",
"Please set the API key in the Options menu before passing a hash.")
return
if not self.current_file_hash:
self.info_text_edit.setText("No hash provided.")
self.info_text_edit.setVisible(True)
return
self.update_virustotal_info()
self.action_toolbar.setVisible(True)
self.view_in_browser_action.setVisible(True)
self.logo_toolbar.setVisible(False)
def update_virustotal_info(self):
self.info_text_edit.setVisible(True)
self.pass_hash_button.setVisible(False)
self.upload_file_button.setVisible(False)
if self.current_file_hash:
data = self.vt_getresult(self.current_file_hash)
if not data: # Check if the data is empty. If empty, it means there was a rate limit error.
self.info_text_edit.setText("Failed to fetch data.")
return
info_text = self.format_data_as_html(data)
self.info_text_edit.setHtml(info_text)
def vt_getresult(self, hashes):
# Check if we're on a new day
if date.today() != self.current_date:
self.current_date = date.today()
self.daily_requests_made = 0
# Check if we've exceeded daily limit
if self.daily_requests_made >= 500:
self.info_text_edit.setPlainText("Daily request limit exceeded. Please try again tomorrow.")
return {}
# Check if we made a request in the last minute
current_time = time()
if current_time - self.last_request_time < 60:
self.requests_made_last_minute += 1
if self.requests_made_last_minute > 3:
# Inform the user about the rate limit with enhanced formatting
self.info_text_edit.setHtml(
''
'
Rate Limit Exceeded
'
'
Please wait a minute and try again.
'
'
Or view in browser.
'
'
'
)
return {}
else:
# If it's been more than a minute since the last request, reset the counter
self.requests_made_last_minute = 1
# Update the last request time and daily requests count
self.last_request_time = current_time
self.daily_requests_made += 1
headers = {
"Accept-Encoding": "gzip, deflate",
"User-Agent": "gzip, My Python requests library example client or username"
}
params = {'apikey': self.api_key, 'resource': hashes}
response = requests_post('https://www.virustotal.com/vtapi/v2/file/report', params=params, headers=headers)
# Handle the case where the response is not a valid JSON (for example, if the rate limit is exceeded)
try:
return response.json()
except RequestException:
self.info_text_edit.setPlainText("Error decoding JSON from the response. Please try again.")
return {}
def format_data_as_html(self, data):
# Extract main details from the data
md5 = data.get('md5', 'N/A')
sha1 = data.get('sha1', 'N/A')
sha256 = data.get('sha256', 'N/A')
scan_date = data.get('scan_date', 'N/A')
positives = data.get('positives', 0)
total = data.get('total', 0)
permalink = data.get('permalink', 'N/A')
# Extract and format the scan results
scans = data.get('scans', {})
scan_rows = ""
for antivirus, result in scans.items():
detected = "Yes" if result.get('detected') else "No"
version = result.get('version', 'N/A')
last_update = result.get('update', 'N/A')
scan_result = result.get('result', 'N/A') or 'N/A'
scan_rows += f"""
| {antivirus} |
{detected} |
{version} |
{last_update} |
{scan_result} |
"""
# Create the HTML content
html_content = f"""
VirusTotal Information
MD5: {md5}
SHA1: {sha1}
SHA256: {sha256}
Last Scanned: {scan_date}
Score: {positives}/{total}
Permalink: {permalink}
Scan Results:
| Antivirus |
Detected |
Version |
Last Update |
Result |
{scan_rows}
"""
return html_content
def view_in_browser(self):
"""Open the VirusTotal URL in the default web browser."""
if self.current_file_hash:
import webbrowser
webbrowser.open(f"https://www.virustotal.com/gui/file/{self.current_file_hash}/detection")
================================================
FILE: requirements.txt
================================================
aiohttp==3.8.5
aiosignal==1.3.1
async-timeout==4.0.3
attrs==23.1.0
av==16.0.0
certifi==2023.7.22
chardet==5.2.0
charset-normalizer==3.3.0
comtypes==1.2.0
docopt==0.6.2
docx==0.2.4
enum-compat==0.0.3
et-xmlfile==1.1.0
filetype==1.2.0
frozenlist==1.4.0
greenlet==3.0.3
hachoir==3.0a3
HTMLParser==0.0.2
hupper==1.12
idna==3.4
Jinja2==3.1.2
libewf-python==20230212
line-profiler==4.1.1
line-profiler-pycharm==1.1.0
lxml==5.1.0
MarkupSafe==2.1.3
multidict==6.0.4
numpy==1.26.3
olefile==0.46
opencv-python==4.10.0.84
openpyxl==3.1.2
pandas==2.2.0
PasteDeploy==3.0.1
Pillow==10.0.0
plaster==1.1.2
plaster-pastedeploy==1.0.1
player==0.6.1
psutil==5.9.5
py-ewf-mount==1.0.0
pycaw==20230407
pycparser==2.21
PyMuPDF==1.23.21
PyMuPDFb==1.23.9
pypdf==4.0.1
PyPDF2==3.0.1
pyramid==2.0.2
pyramid-jinja2==2.10
PySide6==6.5.2
PySide6-Addons==6.5.2
PySide6-Essentials==6.5.2
python-dateutil==2.8.2
python-docx==1.1.0
python-magic-bin==0.4.14
python-pptx==0.6.23
python-registry==1.3.1
python-vlc==3.0.18122
pytsk3==20210801
pytz==2023.3
requests==2.31.0
shiboken6==6.5.2
six==1.16.0
texttable==1.6.7
toml==0.10.2
translationstring==1.4
typing_extensions==4.9.0
tzdata==2023.4
unicodecsv==0.14.1
urllib3==2.0.5
venusian==3.0.0
vulture==2.9.1
WebOb==1.8.7
xlrd==2.0.1
XlsxWriter==3.1.9
yarg==0.1.9
yarl==1.9.2
moviepy~=1.0.3
pdf2image~=1.17.0
================================================
FILE: requirements_macos_silicon.txt
================================================
aiohttp==3.8.5
aiosignal==1.3.1
async-timeout==4.0.3
attrs==23.1.0
av==16.0.0
certifi==2023.7.22
charset-normalizer==3.3.0
comtypes==1.2.0
docopt==0.6.2
docx==0.2.4
enum-compat==0.0.3
et-xmlfile==1.1.0
filetype==1.2.0
frozenlist==1.4.0
greenlet==3.0.3
hachoir==3.0a3
HTMLParser==0.0.2
hupper==1.12
idna==3.4
Jinja2==3.1.2
line-profiler==4.1.1
line-profiler-pycharm==1.1.0
lxml==5.1.0
MarkupSafe==2.1.3
multidict==6.0.4
olefile==0.46
openpyxl==3.1.2
pandas==2.2.0
PasteDeploy==3.0.1
plaster==1.1.2
plaster-pastedeploy==1.0.1
player==0.6.1
psutil==5.9.5
pycaw==20230407
pycparser==2.21
pyramid==2.0.2
pyramid-jinja2==2.10
python-dateutil==2.8.2
python-docx==1.1.0
python-pptx==0.6.23
python-vlc==3.0.18122
requests==2.31.0
six==1.16.0
texttable==1.6.7
toml==0.10.2
translationstring==1.4
typing_extensions==4.9.0
tzdata==2023.4
unicodecsv==0.14.1
urllib3==2.0.5
venusian==3.0.0
vulture==2.9.1
WebOb==1.8.7
xlrd==2.0.1
XlsxWriter==3.1.9
yarg==0.1.9
yarl==1.9.2
numpy==1.26.3
chardet==5.2.0
python-magic==0.4.27
pdf2image==1.17.0
moviepy==1.0.3
PyPDF2==3.0.1
PyMuPDF==1.24.10
PyMuPDFb==1.24.10
Pillow==10.4.0
pytsk3==20231007
libewf-python==20240506
python-registry==1.3.1
PySide6==6.5.2
PySide6-Addons==6.5.2
PySide6-Essentials==6.5.2
opencv-python==4.10.0.84
================================================
FILE: styles/dark_theme.qss
================================================
/* Global Widget Styles */
QWidget {
font-size: 14px;
color: #E0E0E0; /* Light text color */
background-color: #2E2E2E; /* Dark background color */
}
/* Button Styles */
QPushButton {
border: 1px solid #5A5A5A;
border-radius: 2px;
padding: 5px 15px;
background-color: #3C3C3C;
color: #E0E0E0;
margin-left: 8px;
margin-right: 8px;
}
QPushButton:hover {
background-color: #4C4C4C;
}
QPushButton:pressed {
background-color: #5C5C5C;
}
/* CheckBox Styles */
QCheckBox {
spacing: 5px;
color: #E0E0E0;
}
QCheckBox::indicator {
width: 13px;
height: 13px;
border-radius: 6px;
}
QCheckBox::indicator:unchecked {
border: 1px solid #5A5A5A;
background-color: #3C3C3C;
}
QCheckBox::indicator:checked {
image: url('Icons/icons8-tick-48.png');
}
/* ComboBox Styles */
QComboBox {
border: 1px solid #5A5A5A;
border-radius: 2px;
padding: 5px 10px;
background-color: #3C3C3C;
color: #E0E0E0;
selection-background-color: #1E90FF; /* A shade of blue */
}
QComboBox::drop-down {
subcontrol-origin: padding;
subcontrol-position: top right;
width: 25px;
border-left-width: 1px;
border-left-color: #5A5A5A;
border-left-style: solid;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
background-color: #4C4C4C;
}
QComboBox::down-arrow {
image: url('Icons/icons8-dropdown-48.png');
width: 16px;
height: 16px;
}
QComboBox:hover {
border: 1px solid #A2A9B1;
}
QComboBox::drop-down:hover {
background-color: #4C4C4C;
}
/* Dock Widget Styles */
QDockWidget {
border: 1px solid #5A5A5A;
border-radius: 2px;
background-color: #2E2E2E;
}
QDockWidget::title {
background: #3C3C3C;
padding: 1px;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
QDockWidget::close-button, QDockWidget::float-button {
border: 1px solid transparent;
border-radius: 5px;
background: #3C3C3C;
}
QDockWidget::close-button:hover, QDockWidget::float-button:hover {
background: #4C4C4C;
}
QDockWidget::close-button:pressed, QDockWidget::float-button:pressed {
background: #5C5C5C;
}
/* List and Table Widget Styles */
QListWidget, QTableWidget {
border: 1px solid #5A5A5A;
border-radius: 2px;
background-color: #2E2E2E;
color: #E0E0E0;
}
QListWidget::item, QTableWidget::item {
padding: 5px;
}
QListWidget::item:selected, QTableWidget::item:selected {
background-color: #4C4C4C;
color: #E0E0E0;
}
/* Menu and MenuBar Styles */
QMenuBar {
background-color: #3C3C3C;
border-bottom: 1px solid #5A5A5A;
}
QMenuBar::item {
padding: 5px 8px;
border-radius: 2px;
color: #E0E0E0;
}
QMenuBar::item:selected {
background-color: #4C4C4C;
}
QMenuBar::item:pressed {
background-color: #5C5C5C;
}
/* MessageBox Styles */
QMessageBox {
background-color: #2E2E2E;
border: 1px solid #5A5A5A;
color: #E0E0E0;
}
/* ScrollBar Styles */
QScrollBar:vertical {
border: none;
background: #3C3C3C;
width: 10px;
margin: 0px;
border-radius: 0px;
}
QScrollBar::handle:vertical {
background: #5A5A5A;
min-height: 20px;
border-radius: 5px;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
border: none;
background: none;
height: 0px;
}
QScrollBar:horizontal {
border: none;
background: #3C3C3C;
height: 10px;
margin: 0px;
border-radius: 0px;
}
QScrollBar::handle:horizontal {
background: #5A5A5A;
min-width: 20px;
border-radius: 5px;
}
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
border: none;
background: none;
width: 0px;
}
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical,
QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
background: none;
}
QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical,
QScrollBar::left-arrow:horizontal, QScrollBar::right-arrow:horizontal {
background: none;
}
/* Slider Styles */
QSlider::groove:horizontal {
border: 1px solid #5A5A5A;
height: 8px;
background: #3C3C3C;
margin: 2px 0;
border-radius: 4px;
}
QSlider::handle:horizontal {
background: #5A5A5A;
border: 1px solid #5A5A5A;
width: 14px;
margin: -2px 0;
border-radius: 7px;
}
QSlider::handle:horizontal:hover {
background: #6A6A6A;
}
QSlider::handle:horizontal:pressed {
background: #7A7A7A;
}
QSlider::add-page:horizontal {
background: #4C4C4C;
border: 1px solid #5A5A5A;
height: 8px;
border-radius: 4px;
}
/* TabWidget and TabBar Styles */
QTabWidget {
border: 1px solid #5A5A5A;
border-radius: 2px;
background-color: #2E2E2E;
}
QTabWidget::pane {
border-top: 1px solid #5A5A5A; /* Darker gray color to match dark mode */
}
QTabBar::tab {
background: #3C3C3C;
border: 1px solid #5A5A5A;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
min-width: 8ex;
padding: 2px;
color: #E0E0E0;
}
QTabBar::tab:selected, QTabBar::tab:hover {
background: #2E2E2E;
}
QTabBar::tab:selected {
border-color: #9B9B9B;
border-bottom-color: #C2C7CB; /* Separator effect */
}
QTabBar::tab:!selected {
margin-top: 2px; /* Unselected tabs are slightly raised */
}
/* ToolBar Styles */
QToolBar {
background-color: #2E2E2E;
border-bottom: 1px solid #5A5A5A;
padding: 5px;
}
QToolBar::item:hover {
background-color: #4C4C4C;
}
QToolBar::item:pressed {
background-color: #5C5C5C;
}
/* Other Widget Styles */
QLineEdit, QTextEdit, QAudioWidget {
border: 1px solid #5A5A5A;
border-radius: 2px;
background-color: #3C3C3C;
color: #E0E0E0;
}
QLineEdit:hover, QTextEdit:hover {
border: 1px solid #A2A9B1;
}
QLineEdit:focus {
border: 1px solid #1E90FF; /* Blue border on focus */
}
QTreeWidget {
border: 1px solid #5A5A5A;
border-radius: 2px;
background-color: #2E2E2E;
color: #E0E0E0;
}
QTreeWidget::item:selected {
background-color: #505050;
color: #E0E0E0;
}
/* Action Styles */
QAction {
padding: 5px 10px;
border: 1px solid #5A5A5A;
border-radius: 4px;
background-color: #3C3C3C;
color: #E0E0E0;
}
QAction:hover {
background-color: #4C4C4C;
border: 1px solid #A2A9B1;
}
QAction:pressed {
background-color: #5C5C5C;
border: 1px solid #1E90FF;
}
QGroupBox {
border-radius: 4px;
color: #E0E0E0;
}
/* Additional Styles for Consistency */
QHeaderView::section {
background-color: #3C3C3C;
color: #E0E0E0;
padding: 5px;
border-style: none;
border-bottom: 1px solid #5A5A5A;
border-right: 1px solid #5A5A5A;
}
QHeaderView::section:horizontal {
border-top: 1px solid #5A5A5A;
}
QHeaderView::section:vertical {
border-left: 1px solid #5A5A5A;
}
/* Highlighting Selected Items */
QTableWidget::item:selected, QListWidget::item:selected {
background-color: #505050;
color: #E0E0E0;
}
#exportButton {
border: 1px solid #5A5A5A;
border-radius: 2px;
padding: 5px 30px 5px 5px; /* Adjust right padding to push text more to the left */
background-color: #3C3C3C; /* Dark button background */
color: #E0E0E0; /* Light text color */
width: 60px; /* Adjust width as necessary to fit text and menu arrow */
}
#exportButton::menu-button {
subcontrol-origin: padding;
subcontrol-position: top right;
width: 25px;
border-left-width: 1px;
border-left-color: #5A5A5A;
border-left-style: solid;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
background-color: #4C4C4C; /* Slightly lighter background for menu button */
}
#exportButton::menu-button:hover {
background-color: #6A6A6A; /* Lighter shade on hover */
}
#exportButton::menu-arrow {
image: url('Icons/icons8-dropdown-48.png');
width: 16px; /* Adjust the width of the image */
height: 16px; /* Adjust the height of the image */
}
QLabel {
background-color: transparent; /* Set label background to transparent */
}
/* Styles specifically for listingTable QTableWidget in dark theme */
#listingTable {
gridline-color: #5A5A5A;
font-size: 12px;
background-color: #2E2E2E;
color: #E0E0E0; /* Light text color for dark theme */
}
#listingTable::item {
padding: 5px;
color: #E0E0E0; /* Light text color */
background-color: #3C3C3C; /* Darker background for table rows */
}
#listingTable::item:selected {
background-color: #505050; /* Highlight selected items with a contrasting color */
}
#listingTable QHeaderView::section {
background-color: #4C4C4C; /* Dark header background */
color: #E0E0E0; /* Light text color */
padding: 5px;
border-style: none;
border-bottom: 1px solid #3C3C3C;
border-right: 1px solid #3C3C3C;
}
#listingTable QHeaderView::section:horizontal {
border-top: 1px solid #3C3C3C;
margin-top: 0px;
padding-top: 2px;
}
#listingTable QHeaderView::section:vertical {
border-left: 1px solid #3C3C3C;
}
/* QMenu Styles */
QMenu {
background-color: #3C3C3C; /* Dark background for the menu */
border: 1px solid #5A5A5A; /* Border for the menu */
border-radius: 6px; /* Rounded corners for modern look */
padding: 4px; /* Padding around menu */
color: #E0E0E0; /* Text color for the menu */
}
/* QMenu item default style */
QMenu::item {
padding: 5px 25px 5px 10px; /* Padding for menu items */
border-radius: 4px; /* Rounded corners for menu items */
background-color: transparent; /* Transparent background */
color: #E0E0E0; /* Default text color */
}
/* QMenu item hover style */
QMenu::item:hover {
background-color: #505050; /* Background color when hovered */
color: #FFFFFF; /* Text color when hovered */
}
/* QMenu item selected style */
QMenu::item:selected {
background-color: #4C4C4C; /* Background color when selected */
color: #FFFFFF; /* Text color when selected */
}
/* QMenu separator style */
QMenu::separator {
height: 1px;
background-color: #5A5A5A; /* Separator color */
margin: 3px 5px; /* Spacing around separator */
}
/* General styles for all QTableWidgets in dark theme */
QTableWidget, QTreeWidget, QListWidget {
gridline-color: #5A5A5A; /* Gridline color for dark mode */
font-size: 12px;
background-color: #2E2E2E; /* Dark background for tables */
color: #E0E0E0; /* Light text color */
}
/* Table items */
QTableWidget::item, QTreeWidget::item, QListWidget::item {
padding: 5px;
color: #E0E0E0; /* Light text color */
background-color: #3C3C3C; /* Darker background for table rows */
}
QTableWidget::item:selected, QTreeWidget::item:selected, QListWidget::item:selected {
background-color: #505050; /* Highlight selected items with a contrasting color */
}
/* Header styles for all tables and trees */
QHeaderView::section {
background-color: #4C4C4C; /* Dark header background */
color: #E0E0E0; /* Light text color */
padding: 5px;
border-style: none;
border-bottom: 1px solid #3C3C3C;
border-right: 1px solid #3C3C3C;
}
QHeaderView::section:horizontal {
border-top: 1px solid #3C3C3C;
}
QHeaderView::section:vertical {
border-left: 1px solid #3C3C3C;
}
/* Set the alternating row colors for better visibility */
QTableWidget, QTreeWidget, QListWidget {
alternate-background-color: #383838; /* Dark gray for alternating rows */
}
/* Vertical header (row numbers) */
QHeaderView::section:vertical {
background-color: #2E2E2E; /* Same color as table background */
color: #E0E0E0; /* Light text color */
}
/* Styles for scrollbars in dark theme */
QScrollBar:vertical, QScrollBar:horizontal {
background: #3C3C3C; /* Darker background for scrollbars */
width: 10px;
height: 10px;
}
QScrollBar::handle:vertical, QScrollBar::handle:horizontal {
background: #5A5A5A; /* Scroll handle color */
border-radius: 5px;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical,
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
background: none;
height: 0px;
width: 0px;
}
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical,
QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
background: none;
}
#softwareInfoLabel {
font-size: 16pt; /* Slightly larger font for the main title */
font-weight: bold;
color: #ffffff; /* White text for better contrast on dark background */
padding: 10px 0; /* Add some vertical padding */
}
#subtitleLabel {
font-size: 12pt;
color: #c7c7c7; /* Lighter gray for the subtitle */
padding: 5px 0; /* Add a little space around the subtitle */
}
/* Search Results Area Styling for Dark Theme */
#search_results_frame {
background-color: #2E2E2E;
border: 1px solid #5A5A5A;
border-radius: 2px;
padding: 1px;
}
#search_results_title {
background-color: #4C4C4C;
color: #E0E0E0;
padding: 2px;
border-bottom: 1px solid #5A5A5A;
}
#search_results_widget {
background-color: #2E2E2E;
color: #E0E0E0;
border: none;
font-size: 10px;
}
#search_results_widget::item {
padding: 2px;
}
#search_results_widget::item:selected {
background-color: #505050;
}
/* UnifiedViewer Placeholder Label - "No content loaded" message */
#placeholderLabel {
background-color: #3C3C3C; /* Dark gray background */
font-size: 16px;
color: #888888; /* Medium gray text */
padding: 10px;
}
/* AudioVideoPlayer Audio-Only Label - "Playing Audio" message */
#audioOnlyLabel {
background-color: #4C4C4C; /* Slightly lighter dark gray */
font-size: 18px;
color: #B0B0B0; /* Light gray text */
padding: 20px;
}
/* Toolbar Compact Styles - for PictureViewer and PDFViewer */
QToolBar {
spacing: 2px;
padding: 1px;
}
QToolButton {
padding: 2px;
margin: 1px;
}
/* Media Player Sliders - Position and Volume */
QSlider::groove:horizontal {
height: 5px;
margin: 2px 0;
}
QSlider::handle:horizontal {
width: 10px;
margin: -3px 0;
}
/* Search Toolbar Styles (Listing View) */
QLineEdit#listingSearchBar {
border: 1px solid #5A5A5A;
border-radius: 4px;
padding: 6px 10px;
background-color: #3C3C3C;
color: #E0E0E0;
font-size: 13px;
}
QLineEdit#listingSearchBar:hover {
border: 1px solid #A2A9B1;
}
QLineEdit#listingSearchBar:focus {
border: 1px solid #1E90FF;
}
QToolButton#clearSearchBtn {
border: 1px solid #5A5A5A;
border-radius: 4px;
background-color: #3C3C3C;
padding: 4px;
}
QToolButton#clearSearchBtn:hover {
background-color: #4C4C4C;
border: 1px solid #A2A9B1;
}
QToolButton#clearSearchBtn:pressed {
background-color: #5C5C5C;
}
/* File Type Filter GroupBox */
QGroupBox#fileTypeGroup {
border: none;
background-color: transparent;
padding: 0px;
margin: 0px;
}
QGroupBox#fileTypeGroup QCheckBox {
spacing: 6px;
font-size: 12px;
padding: 2px;
}
QGroupBox#fileTypeGroup QCheckBox:hover {
color: #FFFFFF;
}
================================================
FILE: styles/light_theme.qss
================================================
/* Global Widget Styles */
QWidget {
font-size: 14px;
color: #333333;
background-color: #FFFFFF;
}
/* Button Styles */
QPushButton {
border: 1px solid #ced4da;
border-radius: 2px;
padding: 5px 15px;
background-color: #ffffff;
margin-left: 8px;
margin-right: 8px;
}
QPushButton:hover {
background-color: #e7e7e7;
}
QPushButton:pressed {
background-color: #d7d7d7;
}
/* CheckBox Styles */
QCheckBox {
spacing: 5px;
}
QCheckBox::indicator {
width: 13px;
height: 13px;
border-radius: 6px;
}
QCheckBox::indicator:unchecked {
border: 1px solid #d7d7d7;
background-color: #ffffff;
}
QCheckBox::indicator:checked {
image: url('Icons/icons8-tick-48.png');
}
/* ComboBox Styles */
QComboBox {
border: 1px solid #ced4da;
border-radius: 2px;
padding: 5px 10px;
background-color: #ffffff;
selection-background-color: #56CCF2;
}
QComboBox::drop-down {
subcontrol-origin: padding;
subcontrol-position: top right;
width: 25px;
border-left-width: 1px;
border-left-color: #ced4da;
border-left-style: solid;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
QComboBox::down-arrow {
image: url('Icons/icons8-dropdown-48.png');
width: 16px;
height: 16px;
}
QComboBox:hover {
border: 1px solid #a2a9b1;
}
QComboBox::drop-down:hover {
background-color: #f5f5f5;
}
/* Dock Widget Styles */
QDockWidget {
border: 1px solid #E5E5E5;
border-radius: 2px;
background-color: #ffffff;
}
QDockWidget::title {
background: #f5f5f5;
padding: 1px;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
QDockWidget::close-button, QDockWidget::float-button {
border: 1px solid transparent;
border-radius: 5px;
background: #f5f5f5;
}
QDockWidget::close-button:hover, QDockWidget::float-button:hover {
background: #e7e7e7;
}
QDockWidget::close-button:pressed, QDockWidget::float-button:pressed {
background: #d7d7d7;
}
/* Frame Styles */
QFrame {
background-color: #ffffff;
border: none; /* Remove border from all QFrames by default */
}
/* Only apply borders to specific frames where needed */
QFrame#search_results_frame, QDockWidget > QFrame {
border: 1px solid #E5E5E5;
}
/* List and Table Widget Styles */
QListWidget, QTableWidget {
border: 1px solid #E5E5E5;
border-radius: 2px;
background-color: #ffffff;
}
QListWidget::item, QTableWidget::item {
padding: 5px;
}
QListWidget::item:selected, QTableWidget::item:selected {
background-color: #f2f2f2;
color: #333333;
}
/* Menu and MenuBar Styles */
QMenuBar {
background-color: #FFFFFF;
border-bottom: 1px solid #E5E5E5;
}
QMenuBar::item {
padding: 5px 8px;
border-radius: 2px;
}
QMenuBar::item:selected {
background-color: #e7e7e7;
}
QMenuBar::item:pressed {
background-color: #d7d7d7;
}
QMenu {
background-color: #FFFFFF;
border: 1px solid #C0C0C0;
border-radius: 6px;
padding: 4px;
}
QMenu::item {
padding: 5px 25px 5px 10px;
border-radius: 4px;
color: #333333;
}
QMenu::item:selected {
background-color: #e7e7e7;
}
QMenu::item:pressed {
background-color: #d7d7d7;
}
QMenu::separator {
height: 1px;
background-color: #E5E5E5;
margin: 3px 5px;
}
/* MessageBox Styles */
QMessageBox {
background-color: #ffffff;
border: 1px solid #E5E5E5;
}
QScrollBar:vertical {
border: none;
background: #F8F8F8;
width: 10px;
margin: 0px;
border-radius: 0px;
}
QScrollBar::handle:vertical {
background: #C1C1C1;
min-height: 20px;
border-radius: 5px;
}
QScrollBar::add-line:vertical {
border: none;
background: none;
height: 0px;
subcontrol-position: bottom;
subcontrol-origin: margin;
}
QScrollBar::sub-line:vertical {
border: none;
background: none;
height: 0px;
subcontrol-position: top;
subcontrol-origin: margin;
}
QScrollBar:horizontal {
border: none;
background: #F8F8F8;
height: 10px;
margin: 0px;
border-radius: 0px;
}
QScrollBar::handle:horizontal {
background: #C1C1C1;
min-width: 20px;
border-radius: 5px;
}
QScrollBar::add-line:horizontal {
border: none;
background: none;
width: 0px;
subcontrol-position: right;
subcontrol-origin: margin;
}
QScrollBar::sub-line:horizontal {
border: none;
background: none;
width: 0px;
subcontrol-position: left;
subcontrol-origin: margin;
}
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical,
QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
background: none;
}
QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical,
QScrollBar::left-arrow:horizontal, QScrollBar::right-arrow:horizontal {
background: none;
}
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
background: none;
}
/* Slider Styles */
QSlider::groove:horizontal {
border: 1px solid #E5E5E5;
height: 8px;
background: #ffffff;
margin: 2px 0;
border-radius: 4px;
}
QSlider::handle:horizontal {
background: #ffffff;
border: 1px solid #E5E5E5;
width: 14px;
margin: -2px 0;
border-radius: 7px;
}
QSlider::handle:horizontal:hover {
background: #b7b7b7;
}
QSlider::handle:horizontal:pressed {
background: #c7c7c7;
}
QSlider::add-page:horizontal {
background: #e7e7e7;
border: 1px solid #E5E5E5;
height: 8px;
border-radius: 4px;
}
/* TabWidget and TabBar Styles */
QTabWidget {
border: 1px solid #E5E5E5;
border-radius: 2px;
background-color: #ffffff;
}
QTabWidget::pane {
border-top: 1px solid #E5E5E5;
}
QTabBar {
background-color: #FFFFFF;
}
QTabBar::tab {
background: #F5F5F5;
border: 1px solid #E5E5E5;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
min-width: 8ex;
padding: 5px;
color: #333333;
}
QTabBar::tab:selected, QTabBar::tab:hover {
background: #FFFFFF;
}
QTabBar::tab:selected {
border-color: #C2C7CB;
border-bottom-color: #FFFFFF; /* Same as tab background for seamless transition */
}
QTabBar::tab:!selected {
margin-top: 2px; /* Unselected tabs are slightly raised */
}
/* ToolBar Styles */
QToolBar {
background-color: #FFFFFF;
border-bottom: 1px solid #E5E5E5;
padding: 5px;
}
QToolBar::item:hover {
background-color: #e7e7e7;
}
QToolBar::item:pressed {
background-color: #d7d7d7;
}
/* Other Widget Styles */
QLineEdit, QTextEdit, QAudioWidget {
border: 1px solid #E5E5E5;
border-radius: 2px;
background-color: #ffffff;
}
QLineEdit:hover, QTextEdit:hover {
border: 1px solid #a2a9b1;
}
QLineEdit:focus {
border: 1px solid #56CCF2;
}
QTreeWidget {
border: 1px solid #E5E5E5;
border-radius: 2px;
background-color: #ffffff;
}
/* Action Styles */
QAction {
padding: px 10px;
border: 1px solid #ced4da;
border-radius: 4px;
background-color: #ffffff;
}
QAction:hover {
background-color: #f5f5f5;
border: 1px solid #a2a9b1;
}
QAction:pressed {
background-color: #e7e7e7;
border: 1px solid #56CCF2;
}
QGroupBox {
border-radius: 4px;
border: 1px solid #E5E5E5;
background-color: #FFFFFF;
}
#exportButton {
border: 1px solid #ced4da;
border-radius: 2px;
padding: 5px 30px 5px 5px; /* Adjust right padding to push text more to the left */
background-color: #ffffff;
width: 60px; /* Adjust width as necessary to fit text and menu arrow */
}
#exportButton::menu-button {
subcontrol-origin: padding;
subcontrol-position: top right;
width: 25px;
border-left-width: 1px;
border-left-color: #ced4da;
border-left-style: solid;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
}
#exportButton::menu-button:hover {
background-color: #ced4da;
}
#exportButton::menu-arrow {
image: url('Icons/icons8-dropdown-48.png');
width: 16px; /* Adjust the width of the image */
height: 16px; /* Adjust the height of the image */
}
/* General styles for all QTableWidgets, QTreeWidgets, and QListWidgets in light theme */
QTableWidget, QTreeWidget, QListWidget {
gridline-color: #E5E5E5; /* Light gridline color */
font-size: 12px;
background-color: #FFFFFF; /* Light background for tables */
color: #000000; /* Dark text color */
}
/* Table items */
QTableWidget::item, QTreeWidget::item, QListWidget::item {
padding: 5px;
color: #000000; /* Dark text color */
background-color: #FFFFFF; /* White background for table rows */
}
QTableWidget::item:selected, QTreeWidget::item:selected, QListWidget::item:selected {
background-color: #CCE8FF; /* Light blue highlight for selected items */
}
/* Header styles for all tables and trees */
QHeaderView::section {
background-color: #F5F5F5; /* Light gray header background */
color: #000000; /* Dark text color */
padding: 5px;
border-style: none;
border-bottom: 1px solid #E5E5E5;
border-right: 1px solid #E5E5E5;
}
QHeaderView::section:horizontal {
border-top: 1px solid #E5E5E5;
}
/* Specific styles for listingTable's header to remove extra space */
#listingTable QHeaderView::section:horizontal {
border-top: 1px solid #E5E5E5;
margin-top: 0px;
padding-top: 2px;
}
QHeaderView::section:vertical {
border-left: 1px solid #E5E5E5;
}
/* Set the alternating row colors for better visibility */
QTableWidget, QTreeWidget, QListWidget {
alternate-background-color: #F8F8F8; /* Very light gray for alternating rows */
}
/* Vertical header (row numbers) */
QHeaderView::section:vertical {
background-color: #F5F5F5; /* Same color as table background */
color: #000000; /* Dark text color */
}
/* Styles for scrollbars in light theme */
QScrollBar:vertical, QScrollBar:horizontal {
background: #F0F0F0; /* Light background for scrollbars */
width: 10px;
height: 10px;
}
QScrollBar::handle:vertical, QScrollBar::handle:horizontal {
background: #C0C0C0; /* Scroll handle color */
border-radius: 5px;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical,
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
background: none;
height: 0px;
width: 0px;
}
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical,
QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal {
background: none;
}
/* Vertical header style for row numbers */
QHeaderView::section:vertical {
background-color: #F5F5F5; /* Match the table background */
}
/* Styles specifically for lists and tree items in light theme */
QListWidget, QTreeWidget::item {
background-color: #FFFFFF; /* White background for rows */
color: #000000; /* Dark text color */
}
QListWidget::item:selected, QTreeWidget::item:selected {
background-color: #CCE8FF; /* Light blue highlight for selected items */
}
#softwareInfoLabel {
font-size: 16pt; /* Slightly larger font for the main title */
font-weight: bold;
color: #333333; /* Dark text color */
padding: 10px 0; /* Add some vertical padding */
}
#subtitleLabel {
font-size: 12pt;
color: #666666; /* Medium gray for the subtitle */
padding: 5px 0; /* Add a little space around the subtitle */
}
/* Search Results Area Styling for Light Theme */
#search_results_frame {
background-color: #FFFFFF;
border: 1px solid #E5E5E5;
border-radius: 2px;
padding: 1px;
}
#search_results_title {
background-color: #F0F0F0;
color: #333333;
padding: 2px;
border-bottom: 1px solid #D3D3D3;
}
#search_results_widget {
background-color: #FFFFFF;
color: #333333;
border: none;
font-size: 10px;
}
#search_results_widget::item {
padding: 2px;
}
#search_results_widget::item:selected {
background-color: #CCE8FF;
}
/* Address bar in hex view */
QLabel[text="Address"] {
background-color: #F5F5F5;
color: #333333;
}
QLabel[text="00"], QLabel[text="01"], QLabel[text="02"], QLabel[text="03"],
QLabel[text="04"], QLabel[text="05"], QLabel[text="06"], QLabel[text="07"],
QLabel[text="08"], QLabel[text="09"], QLabel[text="0A"], QLabel[text="0B"],
QLabel[text="0C"], QLabel[text="0D"], QLabel[text="0E"], QLabel[text="0F"],
QLabel[text="ASCII"] {
background-color: #F5F5F5;
color: #333333;
}
/* UnifiedViewer Placeholder Label - "No content loaded" message */
#placeholderLabel {
background-color: #F0F0F0; /* Light gray background */
font-size: 16px;
color: #888888; /* Medium gray text */
padding: 10px;
}
/* AudioVideoPlayer Audio-Only Label - "Playing Audio" message */
#audioOnlyLabel {
background-color: #E0E0E0; /* Light gray background */
font-size: 18px;
color: #444444; /* Dark gray text */
padding: 20px;
}
/* Toolbar Compact Styles - for PictureViewer and PDFViewer */
QToolBar {
spacing: 2px;
padding: 1px;
}
QToolButton {
padding: 2px;
margin: 1px;
}
/* Media Player Sliders - Position and Volume */
QSlider::groove:horizontal {
height: 5px;
margin: 2px 0;
}
QSlider::handle:horizontal {
width: 10px;
margin: -3px 0;
}
/* Search Toolbar Styles (Listing View) */
QLineEdit#listingSearchBar {
border: 1px solid #C0C0C0;
border-radius: 4px;
padding: 6px 10px;
background-color: #FFFFFF;
color: #333333;
font-size: 13px;
}
QLineEdit#listingSearchBar:hover {
border: 1px solid #999999;
}
QLineEdit#listingSearchBar:focus {
border: 1px solid #1E90FF;
}
QToolButton#clearSearchBtn {
border: 1px solid #C0C0C0;
border-radius: 4px;
background-color: #F5F5F5;
padding: 4px;
}
QToolButton#clearSearchBtn:hover {
background-color: #E7E7E7;
border: 1px solid #999999;
}
QToolButton#clearSearchBtn:pressed {
background-color: #D7D7D7;
}
/* File Type Filter GroupBox */
QGroupBox#fileTypeGroup {
border: none;
background-color: transparent;
padding: 0px;
margin: 0px;
}
QGroupBox#fileTypeGroup QCheckBox {
spacing: 6px;
font-size: 12px;
padding: 2px;
color: #333333;
}
QGroupBox#fileTypeGroup QCheckBox:hover {
color: #000000;
}
================================================
FILE: tools/Arsenal-Image-Mounter-v3.10.257/Arsenal Recon - End User License Agreement.txt
================================================
ARSENAL RECON END USER LICENSE AGREEMENT
PLEASE READ THIS LICENSE AGREEMENT CAREFULLY BEFORE USING THIS SOFTWARE. BY INSTALLING AND/OR USING THIS SOFTWARE, YOU ACKNOWLEDGE THAT YOU HAVE READ THIS AGREEMENT, UNDERSTAND IT, AND AGREE TO BE BOUND BY ITS TERMS AND CONDITIONS.
Arsenal Consulting, Inc. d/b/a Arsenal Recon (“Arsenal”) is willing to license the enclosed software (the “Software”) to You as the individual, the Company, or the Legal Entity that will be utilizing the Software (“You” or “Your”) only on the condition that You accept all of the terms of this End User License Agreement (“End User License Agreement” or “License Agreement”). This is a legal and enforceable contract between You and Arsenal. By opening this package, breaking the seal, selecting the “I accept the terms in the License Agreement” checkbox or otherwise indicating Your assent electronically, or loading the software, You agree to the terms and conditions of this License Agreement. If You do not agree to these terms and conditions, click the “Cancel” button or otherwise indicate Your refusal, make no further use of the Software, and contact Your vendor or Arsenal for information on how to obtain a refund of the money You paid for the Software (less shipping, handling, and any applicable taxes except in certain states and countries where shipping, handling and taxes are refundable) at any time during the sixty (60) day period following the date of purchase.
Each copy of the Software is individually licensed for use on a single workstation and may be installed on only one workstation at a time. If you have purchased a software-protection dongle, the Software may be installed on as many workstations as You desire as long as it is only used on the workstation that the dongle is currently plugged into.
Copyright & Proprietary Information. The Software (including any accompanying features and services) and documentation (including any product packaging) (the “Documentation”), that accompanies this License Agreement is the property of Arsenal and is protected by copyright law. Your purchase or trial use of the disks or files containing the Software transfers no title to the Software itself. Instead, any use of such Software must be made in accordance with the terms of this License Agreement. Arsenal reserves all rights not specifically granted herein. Your rights and obligations under this License Agreement are set forth below. If You violate such obligations, this License Agreement and Your right to use the Software terminate immediately.
Permitted Uses of a Free Mode License. Under this License, Arsenal grants You the right to use the Software, which may be limited in its functionality or output, and documentation without charge. This License Agreement governs any releases, revisions, updates or enhancements to the Software that Arsenal may make available to You. Under the Free Mode License, Arsenal grants You the right to use the Software on any Workstation.
Prohibited Uses of a Free Mode License. Under this License, You may not (nor may You authorize anyone else to) (i) sell, rent, lend, assign, sublicense or otherwise transfer (completely or partially) the Software or Your rights as expressly provided in this License; (ii) reverse engineer, disassemble, decompile, or make any attempt to discover the source code of the Software.
Permitted Uses of an Educational License. Under this License, Arsenal grants You the right to use the Software and documentation without charge (professors and students in digital forensics programs at colleges and universities) for a specific time period, after which time You will have acquired another Educational license from Arsenal and will continue to be bound by the terms of this Agreement or will have discontinued all use of the Software and destroyed it and all copies thereof, in which event all of Your rights hereunder shall end. During the Educational License period, the Software may be used for educational (and not commercial or other, e.g. casework) purposes only. During the Educational License period, Arsenal grants You the right to: (i) use one copy of the Software on a single Workstation (Arsenal may allow the Software installation to be moved after initial installation); (ii) make one copy of the Software (including all copyright, trademark and proprietary rights notices thereon) for backup purposes only and such copy shall constitute “Software” under this License Agreement.
Prohibited Uses of an Educational License. During the Educational License period You may not (nor may You authorize anyone else to) (i) sell, rent, lend, assign, sublicense or otherwise transfer (completely or partially) the Software or Your rights as expressly provided in this License; (ii) reverse engineer, disassemble, decompile, or make any attempt to discover the source code of the Software; (iii) to make any copy of all or a part of the Software other than one copy (including all copyright, trademark and proprietary rights notices thereon) for backup purposes only and such copy shall constitute “Software” under this License; or (iv) use the Software on a network or across multiple Workstations unless You have a licensed copy of the Software for each Workstation that can access the Software over that network.
Permitted Uses of a Trial License. Under this License, Arsenal grants You the right to use the Software and documentation without charge for a trial period, by which time You will have purchased the software and will continue to be bound by the terms of this Agreement or will have discontinued all use of the Software and destroyed it and all copies thereof, in which event all of Your rights hereunder shall end. During the trial period, the Software may only be used for Your internal testing purposes only. During the trial period, Arsenal grants You the right to: (i) use one copy of the Software on a single Workstation (Arsenal may allow the Software installation to be moved after initial installation); (ii) make one copy of the Software (including all copyright, trademark and proprietary rights notices thereon) for backup purposes only and such copy shall constitute “Software” under this License Agreement.
Prohibited Uses of a Trial License. During the trial period You may not (nor may You authorize anyone else to) (i) sell, rent, lend, assign, sublicense or otherwise transfer (completely or partially) the Software or Your rights as expressly provided in this License; (ii) reverse engineer, disassemble, decompile, or make any attempt to discover the source code of the Software; (iii) to make any copy of all or a part of the Software other than one copy (including all copyright, trademark and proprietary rights notices thereon) for backup purposes only and such copy shall constitute “Software” under this License; or (iv) use the Software on a network or across multiple Workstations unless You have a licensed copy of the Software for each Workstation that can access the Software over that network.
Permitted Uses of a Purchased License. This License Agreement governs any releases, revisions, updates or enhancements to the Software that Arsenal may make available to You. Once fully purchased, under this License Agreement, Arsenal grants You the right to (i) use one copy of the Software on a single Workstation (Arsenal may allow the Software installation to be moved after initial installation); (ii) make one copy of the Software (including all copyright, trademark and proprietary rights notices thereon) for backup purposes only and such copy shall constitute “Software” under this License Agreement; (iii) use the Software on a network, provided that You have a licensed copy of the Software for each Workstation that can access the Software over that network; (iv) permanently transfer all of Your rights in the Software granted under this License Agreement to another person or entity, provided that You retain no copies of the Software and the transferee agrees to the terms of this License Agreement. Partial transfer of Your rights under this License Agreement shall not be permitted. For example, if the applicable documentation grants You the right to use multiple copies of the Software, only a transfer of the rights to use all such copies of the Software would be valid.
Prohibited Uses of a Purchased License. You may not (nor may You authorize anyone else to) (i) sell, rent, lend, assign, sublicense or transfer the Software or Your rights hereunder except as expressly provided in this License Agreement; (ii) make any copy of all or a part of the Software other than the one backup copy or as permitted above; (iii) use the Software as part of a facility management, timesharing, service provider or service bureau arrangement; (iv) publish the software for others to copy; or (v) reverse engineer, disassemble, decompile, or make any attempt to discover the source code of the Software, modify, translate or create derivative works of the Software, any updates, or any part thereof (except as and only to the extent any foregoing restriction is prohibited by applicable law or to the extent as may be permitted by the licensing terms governing use of any open sourced components). Any attempt to do any of the above is a violation of this License Agreement. If You breach any of these restrictions, the License Agreement shall terminate and You may be subject to prosecution and damages.
PRIVACY - PURCHASING AND LICENSING. Arsenal uses third-party services for purchasing and licensing of the Software. Please view their respective privacy policies at http://www.avangate.com/legal/privacy.php and http://www.softworkz.com/privacy.html.
UPDATES. Arsenal reserves the right to modify the Software at any time. Arsenal may distribute modifications or updates to the Software or to portions of the Software to reflect developments. In addition, Arsenal may distribute “bug fixes” or other revisions to the Software. You will be entitled to such updates if Arsenal, in its sole discretion, determines to make such updates available to You (generally within an active software subscription or the first year after purchase, depending on the type of license in question). The terms of this License Agreement will govern any updates unless such update is accompanied by a separate license agreement, in which case the terms of that license will govern.
PRODUCT INSTALLATION AND ACTIVATION There may be technological measures in this Software that are designed to prevent unlicensed or illegal use of the Software. You agree that Arsenal may use these measures to protect against software piracy. This Software may contain enforcement technology that limits Your ability to install and uninstall the Software to not more than a finite number of times for a finite number of computers. This License Agreement and the Software containing enforcement technology may require activation as further set forth in the Documentation. If so, the Software will only operate for a finite period of time prior to Software activation by You. During activation, You may be required to provide Your unique activation code accompanying the Software to verify the authenticity of the Software. If You do not complete the activation within the finite period of time set forth in the Documentation, or as prompted by the Software, the Software will cease to function until activation is complete; at which time the Software functionality will be restored. In the event that You are not able to activate the Software, You may contact Arsenal for assistance.
Termination. The license is effective until terminated by You or Arsenal. You may terminate this License at any time by discontinuing all use of the Software and destroying it and all copies thereof. Your rights under this License will terminate automatically without notice from Arsenal if You fail to comply with any term(s) of this License. Upon termination of the License, You shall cease all use of the Software, and destroy all copies, full or partial, of the Software.
NO WARRANTY: YOU EXPRESSLY ACKNOWLEDGE AND AGREE THAT USE OF THE SOFTWARE IS AT YOUR SOLE RISK AND THAT THE ENTIRE RISK AS TO SATISFACTORY QUALITY, PERFORMANCE, ACCURACY AND EFFORT IS WITH YOU. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE SOFTWARE AND ANY SERVICES PERFORMED OR PROVIDED BY THE SOFTWARE (“SERVICES”) ARE PROVIDED “AS IS” AND “AS AVAILABLE”, WITH ALL FAULTS AND WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, AND ARSENAL HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS WITH RESPECT TO THE SOFTWARE AND ANY SERVICES, EITHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES AND/OR CONDITIONS OF MERCHANTABILITY, OF SATISFACTORY QUALITY, OF FITNESS FOR A PARTICULAR PURPOSE, OF ACCURACY, OF QUIET ENJOYMENT, AND NON-INFRINGEMENT OF THIRD PARTY RIGHTS. ARSENAL DOES NOT WARRANT AGAINST INTERFERENCE WITH YOUR ENJOYMENT OF THE SOFTWARE, THAT THE FUNCTIONS CONTAINED IN, OR SERVICES PERFORMED OR PROVIDED BY, THE SOFTWARE WILL MEET YOUR REQUIREMENTS, THAT THE OPERATION OF THE SOFTWARE OR SERVICES WILL BE UNINTERRUPTED OR ERROR-FREE, OR THAT DEFECTS IN THE SOFTWARE OR SERVICES WILL BE CORRECTED. NO ORAL OR WRITTEN INFORMATION OR ADVICE GIVEN BY ARSENAL OR ITS AUTHORIZED REPRESENTATIVE SHALL CREATE A WARRANTY. SHOULD THE SOFTWARE OR SERVICES PROVE DEFECTIVE, YOU ASSUME THE ENTIRE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES OR LIMITATIONS ON APPLICABLE STATUTORY RIGHTS OF A CONSUMER, SO THE ABOVE EXCLUSION AND LIMITATIONS MAY NOT APPLY TO YOU.
LIMITATION OF LIABILITY: TO THE EXTENT NOT PROHIBITED BY LAW, IN NO EVENT SHALL ARSENAL OR ANY SUPPLIER OR ANY OTHER PERSON INVOLVED IN THE CREATION, PRODUCTION, OR DISTRIBUTION OF THE SOFTWARE BE LIABLE FOR PERSONAL INJURY, OR ANY INCIDENTAL, SPECIAL, INDIRECT, EXEMPLARY, PUNITIVE OR CONSEQUENTIAL DAMAGES WHATSOEVER, INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF PROFITS, LOSS OF DATA, LOSS OF SAVINGS, BUSINESS INTERRUPTION, LOSS OF BUSINESS OR PERSONAL INFORMATION OR ANY OTHER COMMERCIAL DAMAGES OR LOSSES, ARISING OUT OF OR RELATED TO YOUR USE OR INABILITY TO USE THE SOFTWARE OR QUALITY, OR PERFORMANCE OF THE SOFTWARE HOWEVER CAUSED, REGARDLESS OF THE THEORY OF LIABILITY (CONTRACT, TORT OR OTHERWISE) AND EVEN IF ARSENAL HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. SOME JURISDICTIONS DO NOT ALLOW THE LIMITATION OF LIABILITY FOR PERSONAL INJURY, OR OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS LIMITATION MAY NOT APPLY TO YOU. In no event shall Arsenal’s total liability to You for all damages (other than as may be required by applicable law in cases involving personal injury) exceed the purchase price of the Software. The foregoing limitations will apply even if the above stated remedy fails of its essential purpose. These disclaimers shall apply regardless of whether You accept the Software.
The Software is not designed for and is not intended for use in hazardous environments requiring fail-safe (fault-tolerant) performance such as in the operation of nuclear facilities, aircraft navigation or communication systems, air traffic control, weapons or defense systems, life support systems or any other context in which the failure of the Software could lead directly to death, personal injury or severe damage to property or the environment. Arsenal, the Developers of the Software and its suppliers specifically disclaim any express or implied warranty of the Software’s suitability for these types of activities.
EXPORT REGULATION. You acknowledge that the Software and related technical data and services (collectively “Controlled Technology”) may be subject to the import and export laws of the United States, specifically the U.S. Export Administration Regulations (EAR), and the laws of any country where Controlled Technology is imported or re-exported. You agree to comply with all relevant laws and will not export any Controlled Technology in contravention to U.S. law nor to any prohibited country, entity, or person for which an export license or other governmental approval is required. In particular, but without limitation, the Software may not be exported or re-exported (a) into any U.S. embargoed countries or (b) to anyone on the U.S. Treasury Department's list of Specially Designated Nationals or the U.S. Department of Commerce Denied Person’s List or Entity List. By using the Software, You represent and warrant that You are not located in any such country or on any such list. USE OR FACILITATION OF ANY ARSENAL PRODUCT IN CONNECTION WITH ANY ACTIVITY INCLUDING, BUT NOT LIMITED TO, THE DESIGN, DEVELOPMENT, DESIGN, MANUFACTURE, PRODUCTION FABRICATION, TRAINING, OR TESTING OF CHEMICAL, BIOLOGICAL, OR NUCLEAR MATERIALS, OR MISSILES, DRONES, OR SPACE LAUNCH VEHICLES CAPABLE OF DELIVERING WEAPONS OF MASS DESTRUCTION IS PROHIBITED, IN ACCORDANCE WITH U.S. LAW.
Government End Users. The Software and Documentation: Was developed with no government funds; Is a trade secret of Arsenal for all purposes of the Freedom of Information Act. The Software and any associated documentation are provided with RESTRICTED RIGHTS. Use, duplication or disclosure by the Government is subject to restrictions as set forth in subparagraph (c)(1)(ii) of The Rights In Technical Data and Computer Software Clause at 52.227-7013. The Software and related documentation are “Commercial Items”, as that term is defined at 48 C.F.R. §2.101, consisting of “Commercial Computer Software” and “Commercial Computer Software Documentation”, as such terms are used in 48 C.F.R. §12.212 or 48 C.F.R. §227.7202, as applicable. Consistent with 48 C.F.R. §12.212 or 48 C.F.R. §227.7202-1 through 227.7202-4, as applicable, the Commercial Computer Software and Commercial Computer Software Documentation are being licensed to U.S. Government end users (a) only as Commercial Items, and (b) with only those rights as are granted to all other end users pursuant to the terms and conditions herein. Unpublished-rights reserved under the copyright laws of the United States.
General: This License Agreement is the entire agreement between You and Arsenal relating to the Software and: (i) supersedes all prior or contemporaneous oral or written communications, proposals, and representations with respect to its subject matter; and (ii) prevails over any conflicting or additional terms of any quote, order, acknowledgment, or similar communications between the parties. Notwithstanding the foregoing, nothing in this License Agreement will diminish any rights You may have under existing consumer protection legislation or other applicable laws in Your jurisdiction that may not be waived by contract. Arsenal may assign its rights and obligations under this Agreement, without notice, to any affiliate of Arsenal or to any party (or its affiliate) acquiring Arsenal (or any affiliate of Arsenal to which this Agreement has been assigned) or acquiring all or substantially all of the assets to which this Agreement applies. All updates or new versions of the Software which may be received by You from Arsenal shall also be governed by this License Agreement. This License Agreement and Your use of the Software shall be construed, interpreted and governed by the laws of the State of Massachusetts. Your use of the Software may also be subject to other local, state, national, or international laws. If any provision of this License Agreement is found void or unenforceable, it will not affect the validity of the rest of this License. The disclaimers of warranties and damages and limitations on liability shall survive termination. This License Agreement may only be modified by the Documentation or by a written document that has been signed by both You and Arsenal. Headings and captions are for convenience only and are not to be used in the interpretation of this Agreement.
US Government Agencies Bound By Defense Federal Acquisition Regulations. APPLICABLE FLOWDOWNS OF THE PRIME CONTRACT. H.11.3 GSAM 552.232-39 Unenforceability of Unauthorized Obligations (FAR Deviation) (July 2015). (a) Except as stated in paragraph (b) of this clause, when any supply or service acquired under this contract is subject to any commercial supplier agreement (as defined in 502.101) that includes any clause requiring the Government to indemnify the Contractor or any person or entity for damages, costs, fees, or any other loss or liability that would create an Anti-Deficiency Act violation (31 U.S.C. 1341), the following shall govern: (1) Any such clause is unenforceable against the Government. (2) Neither the Government nor any Government authorized end user shall be deemed to have agreed to such clause by virtue of it appearing in the commercial supplier agreement. If the commercial supplier agreement is invoked through an “I agree” click box or other comparable mechanism (e.g., “click-wrap” or “browse-wrap” agreements), execution does not bind the Government or any Government authorized end user to such clause. (3) Any such clause is deemed to be stricken from the commercial supplier agreement. (b) Paragraph (a) of this clause does not apply to indemnification by the Government that is expressly authorized by statute or applicable agency regulations and procedures. (End of Clause) H.11.4 GSAM 552.232-78 Commercial Supplier Agreements – Unenforceable Clauses (a) When any supply or service acquired under this contract is subject to a commercial supplier agreement, the following language shall be deemed incorporated into the commercial supplier agreement. As used herein, “this agreement” means the commercial supplier agreement: (1) Notwithstanding any other provision of this agreement, when the licensee/customer is an agency or instrumentality of the U.S. Government, the following shall apply: (i) Applicability. This agreement is part of a contract between the commercial supplier (or the licensor, where the commercial supplier agreement contains a license) and the U.S. Government for the acquisition of the supply or service 6 (including all contracts, task orders, and delivery orders under FAR Parts 13, 14 or 15). (ii) Licensee or Customer. This agreement shall bind the ordering activity as licensee or customer but shall not operate to bind a Government employee or person acting on behalf of the Government in his or her personal capacity. (iii) Law and disputes. This agreement is governed by Federal law. (A) Any provision purporting to subject the U.S. Government to the laws of a U.S. state, U.S. territory, district, or municipality, or foreign nation, except where Federal law expressly provides for the application of such laws, is hereby deleted. (B) Any provision requiring dispute resolution in a specific forum or venue that is different from that prescribed by applicable Federal law is hereby deleted. (C) Any provision prescribing a different time period for bringing an action than that prescribed by applicable Federal law is hereby deleted. (iv) Continued performance. In accordance with subparagraph (i) of the clause at 52.233-1 (Disputes), this agreement may not be unilaterally terminated by the commercial supplier or licensor, and the provision of supplies or services under this agreement may not be unilaterally suspended unless generally withdrawn from the commercial market. If the supplier or licensor believes the ordering activity to be in breach of the agreement, it shall pursue its rights under the Contract Disputes Act or other applicable Federal statute while continuing performance as set forth in such subparagraph (i). (v) Arbitration; equitable or injunctive relief. In the event of a claim or dispute arising under or relating to this agreement, (A) binding arbitration shall not be used unless specifically authorized by agency guidance, and (B) equitable or injunctive relief, including the award of attorney fees, costs or interest, may be awarded against the U.S. Government only when explicitly provided by statute (e.g., Prompt Payment Act or Equal Access to Justice Act). (vi) No additional terms. No other commercial supplier terms shall bind the Government unless included verbatim (not by reference) in the commercial supplier agreement and added to the Government contract or order with the approval of the cognizant contracting officer. (vii) No unilateral changes. Any clause of this agreement claiming that one party to the agreement may unilaterally change any provision of this agreement shall not apply. (viii) No automatic renewals. If any license or service tied to periodic payment is provided under this agreement (e.g., annual software maintenance or annual lease term), such license or service shall not renew automatically upon expiration of its current term without prior express Government approval. (ix) Indemnification. Any clause of this agreement requiring the commercial supplier or licensor to defend or indemnify the end user is hereby amended to provide that representation in the conduct of litigation in which the United States is a party or is interested is reserved for the U.S. Department of Justice in accordance with 28 U.S.C. 516. (x) Audits. Any clause of this agreement permitting the commercial supplier or licensor to audit the end user’s compliance with this agreement is hereby amended as follows: (A) Discrepancies found in an audit may result in a charge by the commercial supplier or licensor to the ordering activity. Any resulting invoice must comply with the proper invoicing requirements specified in the underlying Government contract or order. (B) This charge, if disputed by the ordering activity, will be resolved through the Disputes clause at 52.233-1; no payment obligation shall arise on the part of the ordering activity until the conclusion of the dispute process. (C) Any audit requested by the commercial supplier or licensor will be performed at the commercial supplier’s or licensor’s expense, without reimbursement by the Government. (xi) Taxes or surcharges. Any taxes or surcharges which the commercial supplier or licensor seeks to pass along to the Government as end user will be governed by the terms of the underlying Government contract or order and, in any event, must be submitted to the Contracting Officer for a determination of applicability prior to invoicing. (xii) Non-assignment. This agreement may not be assigned, nor may any rights or obligations thereunder be delegated, without the Government's prior approval, except as expressly permitted under the clause at 52.232-23, Assignment of Claims. (xiii) Confidential information. If this agreement includes a confidentiality clause, such clause is hereby amended to state that (A) neither this agreement nor the final pricing agreed to by the ordering activity in the underlying Government contract or order shall be deemed "confidential information" 8 notwithstanding marking to that effect; and (B) notwithstanding anything in this agreement to the contrary, the Government may retain any confidential information as required by law, regulation or its bona fide internal document retention procedures for legal, regulatory or compliance purposes; provided, however, that all such retained confidential information will continue to be subject to the confidentiality obligations of this agreement. (b) If any provision of this agreement conflicts or is inconsistent with the preceding subparagraph (a)(1), the provisions of subparagraph (a)(1) shall prevail to the extent of such inconsistency. (End of Clause)
CONTACT: Arsenal Consulting, 120 Eastern Avenue, Unit 7, Chelsea, Massachusetts 02150;
Tel (617) ARSENAL or (617) 277-3625
Copyright (C) 2023 Arsenal Consulting, Inc. All rights reserved.
================================================
FILE: tools/Arsenal-Image-Mounter-v3.10.257/ArsenalImageMounter.deps.json
================================================
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v6.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v6.0": {
"ArsenalImageMounter/3.10": {
"dependencies": {
"Arsenal.ImageMounter.Cli": "3.10.257",
"Microsoft.Win32.TaskScheduler": "2.9.1.0"
},
"runtime": {
"ArsenalImageMounter.dll": {}
},
"resources": {
"de/ArsenalImageMounter.resources.dll": {
"locale": "de"
},
"fr/ArsenalImageMounter.resources.dll": {
"locale": "fr"
},
"sv/ArsenalImageMounter.resources.dll": {
"locale": "sv"
}
}
},
"Arsenal.ImageMounter/3.10.257": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28",
"LTRData.DiscUtils.Dmg": "1.0.28",
"LTRData.DiscUtils.Fat": "1.0.28",
"LTRData.DiscUtils.Ntfs": "1.0.28",
"LTRData.DiscUtils.OpticalDisk": "1.0.28",
"LTRData.DiscUtils.Streams": "1.0.28",
"LTRData.DiscUtils.Vdi": "1.0.28",
"LTRData.DiscUtils.Vhd": "1.0.28",
"LTRData.DiscUtils.Vhdx": "1.0.28",
"LTRData.DiscUtils.Vmdk": "1.0.28",
"LTRData.DiscUtils.Xva": "1.0.28",
"Microsoft.Bcl.HashCode": "1.1.1",
"Microsoft.Win32.Registry": "5.0.0",
"System.Buffers": "4.5.1",
"System.IO.FileSystem.AccessControl": "5.0.0",
"System.Memory": "4.5.5",
"System.ServiceProcess.ServiceController": "7.0.1",
"System.Threading.Tasks.Extensions": "4.5.4"
},
"runtime": {
"lib/net6.0/Arsenal.ImageMounter.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "3.10.257.0"
}
}
},
"Arsenal.ImageMounter.Cli/3.10.257": {
"dependencies": {
"Arsenal.ImageMounter": "3.10.257",
"System.Text.Json": "7.0.3"
},
"runtime": {
"lib/net6.0/aim_cli.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "3.10.257.0"
}
}
},
"Arsenal.ImageMounter.Forms/3.10.257": {
"dependencies": {
"Arsenal.ImageMounter": "3.10.257",
"Microsoft.Win32.Registry": "5.0.0",
"System.IO.FileSystem.AccessControl": "5.0.0",
"System.Memory": "4.5.5",
"System.ServiceProcess.ServiceController": "7.0.1"
},
"runtime": {
"lib/net6.0-windows7.0/Arsenal.ImageMounter.Forms.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "3.10.257.0"
}
}
},
"LTRData.DiscUtils.BootConfig/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28",
"LTRData.DiscUtils.Registry": "1.0.28"
},
"runtime": {
"lib/net6.0/DiscUtils.BootConfig.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Btrfs/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28",
"LTRData.DiscUtils.Streams": "1.0.28",
"lzo.net": "0.0.6"
},
"runtime": {
"lib/net6.0/DiscUtils.Btrfs.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Core/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Streams": "1.0.28",
"Microsoft.Bcl.HashCode": "1.1.1",
"System.Text.Encoding.CodePages": "7.0.0"
},
"runtime": {
"lib/net6.0/DiscUtils.Core.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Dmg/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28",
"lzfse-net": "1.0.15"
},
"runtime": {
"lib/net6.0/DiscUtils.Dmg.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Ext/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28"
},
"runtime": {
"lib/net6.0/DiscUtils.Ext.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Fat/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28",
"System.Text.Encoding.CodePages": "7.0.0"
},
"runtime": {
"lib/net6.0/DiscUtils.Fat.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.HfsPlus/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28"
},
"runtime": {
"lib/net6.0/DiscUtils.HfsPlus.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Iso9660/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28",
"Microsoft.Bcl.HashCode": "1.1.1"
},
"runtime": {
"lib/net6.0/DiscUtils.Iso9660.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Lvm/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28"
},
"runtime": {
"lib/net6.0/DiscUtils.Lvm.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Nfs/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28",
"Microsoft.Bcl.HashCode": "1.1.1"
},
"runtime": {
"lib/net6.0/DiscUtils.Nfs.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Ntfs/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28",
"Microsoft.Bcl.HashCode": "1.1.1",
"System.ValueTuple": "4.5.0"
},
"runtime": {
"lib/net6.0/DiscUtils.Ntfs.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.OpticalDisk/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28",
"LTRData.DiscUtils.Iso9660": "1.0.28",
"LTRData.DiscUtils.Udf": "1.0.28"
},
"runtime": {
"lib/net6.0/DiscUtils.OpticalDisk.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Registry/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28",
"LTRData.DiscUtils.Streams": "1.0.28",
"System.Memory": "4.5.5"
},
"runtime": {
"lib/net6.0/DiscUtils.Registry.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.SquashFs/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28"
},
"runtime": {
"lib/net6.0/DiscUtils.SquashFs.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Streams/1.0.28": {
"dependencies": {
"Microsoft.Bcl.HashCode": "1.1.1",
"System.Memory": "4.5.5",
"System.Security.Cryptography.Algorithms": "4.3.1",
"System.Threading.Tasks.Extensions": "4.5.4"
},
"runtime": {
"lib/net6.0/DiscUtils.Streams.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Swap/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28"
},
"runtime": {
"lib/net6.0/DiscUtils.Swap.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Udf/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28",
"LTRData.DiscUtils.Iso9660": "1.0.28"
},
"runtime": {
"lib/net6.0/DiscUtils.Udf.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Vdi/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28"
},
"runtime": {
"lib/net6.0/DiscUtils.Vdi.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Vhd/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28",
"System.ValueTuple": "4.5.0"
},
"runtime": {
"lib/net6.0/DiscUtils.Vhd.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Vhdx/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28",
"System.ValueTuple": "4.5.0"
},
"runtime": {
"lib/net6.0/DiscUtils.Vhdx.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.VirtualFileSystem/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28"
},
"runtime": {
"lib/net6.0/DiscUtils.VirtualFileSystem.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Vmdk/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28",
"System.ValueTuple": "4.5.0"
},
"runtime": {
"lib/net6.0/DiscUtils.Vmdk.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Wim/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28"
},
"runtime": {
"lib/net6.0/DiscUtils.Wim.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Xfs/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28"
},
"runtime": {
"lib/net6.0/DiscUtils.Xfs.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.DiscUtils.Xva/1.0.28": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28",
"System.ValueTuple": "4.5.0"
},
"runtime": {
"lib/net6.0/DiscUtils.Xva.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.28.0"
}
}
},
"LTRData.ExFat.Core/1.0.10": {
"dependencies": {
"LTRData.DiscUtils.Streams": "1.0.28",
"System.Memory": "4.5.5",
"System.Threading.Tasks.Extensions": "4.5.4",
"System.ValueTuple": "4.5.0"
},
"runtime": {
"lib/net6.0/ExFat.Core.dll": {
"assemblyVersion": "0.9.18.0",
"fileVersion": "0.9.18.0"
}
}
},
"LTRData.ExFat.DiscUtils/1.0.10": {
"dependencies": {
"LTRData.DiscUtils.Core": "1.0.28",
"LTRData.DiscUtils.Streams": "1.0.28",
"LTRData.ExFat.Core": "1.0.10"
},
"runtime": {
"lib/net6.0/ExFat.DiscUtils.dll": {
"assemblyVersion": "0.9.18.0",
"fileVersion": "0.9.18.0"
}
}
},
"lzfse-net/1.0.15": {
"runtime": {
"lib/netstandard2.0/lzfse-net.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.0.15.34350"
}
},
"runtimeTargets": {
"runtimes/linux-arm64/native/liblzfse.so": {
"rid": "linux-arm64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-musl-x64/native/liblzfse.so": {
"rid": "linux-musl-x64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/linux-x64/native/liblzfse.so": {
"rid": "linux-x64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/osx-x64/native/liblzfse.dylib": {
"rid": "osx-x64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/win-x64/native/lzfse.dll": {
"rid": "win-x64",
"assetType": "native",
"fileVersion": "0.0.0.0"
},
"runtimes/win-x86/native/lzfse.dll": {
"rid": "win-x86",
"assetType": "native",
"fileVersion": "0.0.0.0"
}
}
},
"lzo.net/0.0.6": {
"runtime": {
"lib/netstandard2.0/lzo.net.dll": {
"assemblyVersion": "0.0.6.0",
"fileVersion": "0.0.6.0"
}
}
},
"Microsoft.Bcl.HashCode/1.1.1": {
"runtime": {
"lib/netcoreapp2.1/Microsoft.Bcl.HashCode.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "4.700.20.56604"
}
}
},
"Microsoft.Management.Infrastructure/2.0.0": {
"dependencies": {
"Microsoft.Management.Infrastructure.Runtime.Unix": "2.0.0",
"Microsoft.Management.Infrastructure.Runtime.Win": "2.0.0"
}
},
"Microsoft.Management.Infrastructure.Runtime.Unix/2.0.0": {
"runtimeTargets": {
"runtimes/unix/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll": {
"rid": "unix",
"assetType": "runtime",
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
}
}
},
"Microsoft.Management.Infrastructure.Runtime.Win/2.0.0": {
"runtimeTargets": {
"runtimes/win-arm/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll": {
"rid": "win-arm",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win-arm/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll": {
"rid": "win-arm",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win-arm64/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll": {
"rid": "win-arm64",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win-arm64/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll": {
"rid": "win-arm64",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win10-x64/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll": {
"rid": "win10-x64",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win10-x64/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll": {
"rid": "win10-x64",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win10-x86/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll": {
"rid": "win10-x86",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win10-x86/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll": {
"rid": "win10-x86",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win7-x64/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll": {
"rid": "win7-x64",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win7-x64/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll": {
"rid": "win7-x64",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win7-x86/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll": {
"rid": "win7-x86",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win7-x86/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll": {
"rid": "win7-x86",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win8-x64/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll": {
"rid": "win8-x64",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win8-x64/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll": {
"rid": "win8-x64",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win8-x86/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll": {
"rid": "win8-x86",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win8-x86/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll": {
"rid": "win8-x86",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win81-x64/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll": {
"rid": "win81-x64",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win81-x64/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll": {
"rid": "win81-x64",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win81-x86/lib/netstandard1.6/Microsoft.Management.Infrastructure.Native.dll": {
"rid": "win81-x86",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win81-x86/lib/netstandard1.6/Microsoft.Management.Infrastructure.dll": {
"rid": "win81-x86",
"assetType": "runtime",
"assemblyVersion": "1.0.0.0",
"fileVersion": "10.0.18362.1"
},
"runtimes/win-arm/native/Microsoft.Management.Infrastructure.Native.Unmanaged.dll": {
"rid": "win-arm",
"assetType": "native",
"fileVersion": "10.0.18362.1"
},
"runtimes/win-arm/native/mi.dll": {
"rid": "win-arm",
"assetType": "native",
"fileVersion": "10.0.18362.1"
},
"runtimes/win-arm/native/miutils.dll": {
"rid": "win-arm",
"assetType": "native",
"fileVersion": "10.0.18362.1"
},
"runtimes/win-arm64/native/Microsoft.Management.Infrastructure.Native.Unmanaged.dll": {
"rid": "win-arm64",
"assetType": "native",
"fileVersion": "10.0.18362.1"
},
"runtimes/win-arm64/native/mi.dll": {
"rid": "win-arm64",
"assetType": "native",
"fileVersion": "10.0.18362.1"
},
"runtimes/win-arm64/native/miutils.dll": {
"rid": "win-arm64",
"assetType": "native",
"fileVersion": "10.0.18362.1"
},
"runtimes/win10-x64/native/Microsoft.Management.Infrastructure.Native.Unmanaged.dll": {
"rid": "win10-x64",
"assetType": "native",
"fileVersion": "10.0.18362.1"
},
"runtimes/win10-x64/native/mi.dll": {
"rid": "win10-x64",
"assetType": "native",
"fileVersion": "10.0.18362.1"
},
"runtimes/win10-x64/native/miutils.dll": {
"rid": "win10-x64",
"assetType": "native",
"fileVersion": "10.0.18362.1"
},
"runtimes/win10-x86/native/Microsoft.Management.Infrastructure.Native.Unmanaged.dll": {
"rid": "win10-x86",
"assetType": "native",
"fileVersion": "10.0.18362.1"
},
"runtimes/win10-x86/native/mi.dll": {
"rid": "win10-x86",
"assetType": "native",
"fileVersion": "10.0.18362.1"
},
"runtimes/win10-x86/native/miutils.dll": {
"rid": "win10-x86",
"assetType": "native",
"fileVersion": "10.0.18362.1"
},
"runtimes/win7-x64/native/Microsoft.Management.Infrastructure.Native.Unmanaged.dll": {
"rid": "win7-x64",
"assetType": "native",
"fileVersion": "10.0.14394.1000"
},
"runtimes/win7-x64/native/mi.dll": {
"rid": "win7-x64",
"assetType": "native",
"fileVersion": "10.0.14394.1000"
},
"runtimes/win7-x64/native/miutils.dll": {
"rid": "win7-x64",
"assetType": "native",
"fileVersion": "10.0.14394.1000"
},
"runtimes/win7-x86/native/Microsoft.Management.Infrastructure.Native.Unmanaged.dll": {
"rid": "win7-x86",
"assetType": "native",
"fileVersion": "10.0.14394.1000"
},
"runtimes/win7-x86/native/mi.dll": {
"rid": "win7-x86",
"assetType": "native",
"fileVersion": "10.0.14394.1000"
},
"runtimes/win7-x86/native/miutils.dll": {
"rid": "win7-x86",
"assetType": "native",
"fileVersion": "10.0.14394.1000"
},
"runtimes/win8-x64/native/mi.dll": {
"rid": "win8-x64",
"assetType": "native",
"fileVersion": "6.2.9200.22812"
},
"runtimes/win8-x64/native/miutils.dll": {
"rid": "win8-x64",
"assetType": "native",
"fileVersion": "6.2.9200.22812"
},
"runtimes/win8-x86/native/mi.dll": {
"rid": "win8-x86",
"assetType": "native",
"fileVersion": "6.2.9200.22812"
},
"runtimes/win8-x86/native/miutils.dll": {
"rid": "win8-x86",
"assetType": "native",
"fileVersion": "6.2.9200.22812"
},
"runtimes/win81-x64/native/Microsoft.Management.Infrastructure.Native.Unmanaged.dll": {
"rid": "win81-x64",
"assetType": "native",
"fileVersion": "6.3.9600.16384"
},
"runtimes/win81-x64/native/mi.dll": {
"rid": "win81-x64",
"assetType": "native",
"fileVersion": "6.3.9600.16384"
},
"runtimes/win81-x64/native/miutils.dll": {
"rid": "win81-x64",
"assetType": "native",
"fileVersion": "6.3.9600.16384"
},
"runtimes/win81-x86/native/Microsoft.Management.Infrastructure.Native.Unmanaged.dll": {
"rid": "win81-x86",
"assetType": "native",
"fileVersion": "6.3.9600.16384"
},
"runtimes/win81-x86/native/mi.dll": {
"rid": "win81-x86",
"assetType": "native",
"fileVersion": "6.3.9600.16384"
},
"runtimes/win81-x86/native/miutils.dll": {
"rid": "win81-x86",
"assetType": "native",
"fileVersion": "6.3.9600.16384"
}
}
},
"Microsoft.NETCore.Platforms/5.0.0": {},
"Microsoft.NETCore.Targets/1.1.0": {},
"Microsoft.Win32.Registry/5.0.0": {
"dependencies": {
"System.Security.AccessControl": "5.0.0",
"System.Security.Principal.Windows": "5.0.0"
}
},
"Microsoft.Win32.SystemEvents/4.7.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0"
}
},
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {},
"runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {},
"runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {},
"runtime.native.System.Security.Cryptography.Apple/4.3.1": {
"dependencies": {
"runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.1"
}
},
"runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {
"dependencies": {
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2",
"runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2",
"runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2",
"runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2",
"runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2",
"runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2",
"runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2",
"runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2",
"runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2",
"runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2"
}
},
"runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {},
"runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {},
"runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple/4.3.1": {},
"runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {},
"runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {},
"runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {},
"runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {},
"runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {},
"System.Buffers/4.5.1": {},
"System.CodeDom/4.7.0": {},
"System.Collections/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Collections.Concurrent/4.3.0": {
"dependencies": {
"System.Collections": "4.3.0",
"System.Diagnostics.Debug": "4.3.0",
"System.Diagnostics.Tracing": "4.3.0",
"System.Globalization": "4.3.0",
"System.Reflection": "4.3.0",
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0",
"System.Runtime.Extensions": "4.3.0",
"System.Threading": "4.3.0",
"System.Threading.Tasks": "4.3.0"
}
},
"System.Diagnostics.Debug/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Diagnostics.EventLog/7.0.0": {
"runtime": {
"lib/net6.0/System.Diagnostics.EventLog.dll": {
"assemblyVersion": "7.0.0.0",
"fileVersion": "7.0.22.51805"
}
},
"runtimeTargets": {
"runtimes/win/lib/net6.0/System.Diagnostics.EventLog.Messages.dll": {
"rid": "win",
"assetType": "runtime",
"assemblyVersion": "7.0.0.0",
"fileVersion": "0.0.0.0"
},
"runtimes/win/lib/net6.0/System.Diagnostics.EventLog.dll": {
"rid": "win",
"assetType": "runtime",
"assemblyVersion": "7.0.0.0",
"fileVersion": "7.0.22.51805"
}
}
},
"System.Diagnostics.Tracing/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Drawing.Common/4.7.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"Microsoft.Win32.SystemEvents": "4.7.0"
}
},
"System.Globalization/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.IO/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0",
"System.Text.Encoding": "4.3.0",
"System.Threading.Tasks": "4.3.0"
}
},
"System.IO.FileSystem.AccessControl/5.0.0": {
"dependencies": {
"System.Security.AccessControl": "5.0.0",
"System.Security.Principal.Windows": "5.0.0"
}
},
"System.Linq/4.3.0": {
"dependencies": {
"System.Collections": "4.3.0",
"System.Diagnostics.Debug": "4.3.0",
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0",
"System.Runtime.Extensions": "4.3.0"
}
},
"System.Management/4.7.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"Microsoft.Win32.Registry": "5.0.0",
"System.CodeDom": "4.7.0"
},
"runtime": {
"lib/netstandard2.0/System.Management.dll": {
"assemblyVersion": "4.0.1.0",
"fileVersion": "4.700.19.56404"
}
},
"runtimeTargets": {
"runtimes/win/lib/netcoreapp2.0/System.Management.dll": {
"rid": "win",
"assetType": "runtime",
"assemblyVersion": "4.0.1.0",
"fileVersion": "4.700.19.56404"
}
}
},
"System.Memory/4.5.5": {},
"System.Reflection/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.IO": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Reflection.Primitives/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Resources.Extensions/7.0.0": {
"runtime": {
"lib/net6.0/System.Resources.Extensions.dll": {
"assemblyVersion": "7.0.0.0",
"fileVersion": "7.0.22.51805"
}
}
},
"System.Resources.ResourceManager/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Globalization": "4.3.0",
"System.Reflection": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Runtime/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"Microsoft.NETCore.Targets": "1.1.0"
}
},
"System.Runtime.CompilerServices.Unsafe/6.0.0": {},
"System.Runtime.Extensions/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Runtime.Handles/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Runtime.InteropServices/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Reflection": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Runtime": "4.3.0",
"System.Runtime.Handles": "4.3.0"
}
},
"System.Runtime.Numerics/4.3.0": {
"dependencies": {
"System.Globalization": "4.3.0",
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0",
"System.Runtime.Extensions": "4.3.0"
}
},
"System.Security.AccessControl/5.0.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"System.Security.Principal.Windows": "5.0.0"
}
},
"System.Security.Cryptography.Algorithms/4.3.1": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"System.Collections": "4.3.0",
"System.IO": "4.3.0",
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0",
"System.Runtime.Extensions": "4.3.0",
"System.Runtime.Handles": "4.3.0",
"System.Runtime.InteropServices": "4.3.0",
"System.Runtime.Numerics": "4.3.0",
"System.Security.Cryptography.Encoding": "4.3.0",
"System.Security.Cryptography.Primitives": "4.3.0",
"System.Text.Encoding": "4.3.0",
"runtime.native.System.Security.Cryptography.Apple": "4.3.1",
"runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2"
}
},
"System.Security.Cryptography.Encoding/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"System.Collections": "4.3.0",
"System.Collections.Concurrent": "4.3.0",
"System.Linq": "4.3.0",
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0",
"System.Runtime.Extensions": "4.3.0",
"System.Runtime.Handles": "4.3.0",
"System.Runtime.InteropServices": "4.3.0",
"System.Security.Cryptography.Primitives": "4.3.0",
"System.Text.Encoding": "4.3.0",
"runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2"
}
},
"System.Security.Cryptography.Primitives/4.3.0": {
"dependencies": {
"System.Diagnostics.Debug": "4.3.0",
"System.Globalization": "4.3.0",
"System.IO": "4.3.0",
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0",
"System.Threading": "4.3.0",
"System.Threading.Tasks": "4.3.0"
}
},
"System.Security.Permissions/4.7.0": {
"dependencies": {
"System.Security.AccessControl": "5.0.0",
"System.Windows.Extensions": "4.7.0"
}
},
"System.Security.Principal.Windows/5.0.0": {},
"System.ServiceProcess.ServiceController/7.0.1": {
"dependencies": {
"System.Diagnostics.EventLog": "7.0.0"
},
"runtime": {
"lib/net6.0/System.ServiceProcess.ServiceController.dll": {
"assemblyVersion": "7.0.0.1",
"fileVersion": "7.0.723.27404"
}
},
"runtimeTargets": {
"runtimes/win/lib/net6.0/System.ServiceProcess.ServiceController.dll": {
"rid": "win",
"assetType": "runtime",
"assemblyVersion": "7.0.0.1",
"fileVersion": "7.0.723.27404"
}
}
},
"System.Text.Encoding/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Text.Encoding.CodePages/7.0.0": {
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
},
"runtime": {
"lib/net6.0/System.Text.Encoding.CodePages.dll": {
"assemblyVersion": "7.0.0.0",
"fileVersion": "7.0.22.51805"
}
},
"runtimeTargets": {
"runtimes/win/lib/net6.0/System.Text.Encoding.CodePages.dll": {
"rid": "win",
"assetType": "runtime",
"assemblyVersion": "7.0.0.0",
"fileVersion": "7.0.22.51805"
}
}
},
"System.Text.Encodings.Web/7.0.0": {
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
},
"runtime": {
"lib/net6.0/System.Text.Encodings.Web.dll": {
"assemblyVersion": "7.0.0.0",
"fileVersion": "7.0.22.51805"
}
},
"runtimeTargets": {
"runtimes/browser/lib/net6.0/System.Text.Encodings.Web.dll": {
"rid": "browser",
"assetType": "runtime",
"assemblyVersion": "7.0.0.0",
"fileVersion": "7.0.22.51805"
}
}
},
"System.Text.Json/7.0.3": {
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "6.0.0",
"System.Text.Encodings.Web": "7.0.0"
},
"runtime": {
"lib/net6.0/System.Text.Json.dll": {
"assemblyVersion": "7.0.0.0",
"fileVersion": "7.0.723.27404"
}
}
},
"System.Threading/4.3.0": {
"dependencies": {
"System.Runtime": "4.3.0",
"System.Threading.Tasks": "4.3.0"
}
},
"System.Threading.Tasks/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Threading.Tasks.Extensions/4.5.4": {},
"System.ValueTuple/4.5.0": {},
"System.Windows.Extensions/4.7.0": {
"dependencies": {
"System.Drawing.Common": "4.7.0"
}
},
"Microsoft.Win32.TaskScheduler/2.9.1.0": {
"runtime": {
"Microsoft.Win32.TaskScheduler.dll": {
"assemblyVersion": "2.9.1.0",
"fileVersion": "2.9.1.0"
}
}
}
}
},
"libraries": {
"ArsenalImageMounter/3.10": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Arsenal.ImageMounter/3.10.257": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Q0V2D2eIXvHyDKK092xduJTWdUtubjVp1PB3jhoDvHayVwKjh8bz83KLQcYYaJN16ul0z61O28Pdh8te2p5gOw==",
"path": "arsenal.imagemounter/3.10.257",
"hashPath": "arsenal.imagemounter.3.10.257.nupkg.sha512"
},
"Arsenal.ImageMounter.Cli/3.10.257": {
"type": "package",
"serviceable": true,
"sha512": "sha512-h0ZELwbcHG9/EoosMAK9GhYuBzuizH4rw6rpooh1amzoXzXSp53dd9wahtbhBv5lKIvd9p4g4LhEutOZwT4t/w==",
"path": "arsenal.imagemounter.cli/3.10.257",
"hashPath": "arsenal.imagemounter.cli.3.10.257.nupkg.sha512"
},
"Arsenal.ImageMounter.Forms/3.10.257": {
"type": "package",
"serviceable": true,
"sha512": "sha512-hOi4lbM+VTbnuwBabu3RIJEdqRWniPfNZImNdK3s6XAIs0/29edOvQ088BiQSmuLF7AlI8gl2zdGlgJ1LsCNCA==",
"path": "arsenal.imagemounter.forms/3.10.257",
"hashPath": "arsenal.imagemounter.forms.3.10.257.nupkg.sha512"
},
"LTRData.DiscUtils.BootConfig/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-jbdyr3N3WOSSuAIdJEv0lzWLqRTsGP6HyWIiovGo4BkF65Mj6jgm9qsfYGDRrUYnx3ME1mx2UGhSB0zGjUey2A==",
"path": "ltrdata.discutils.bootconfig/1.0.28",
"hashPath": "ltrdata.discutils.bootconfig.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Btrfs/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-043Jbrsn9ufjuYFI7+1/elCBEn+mRX0j+Ra9Jati/87FHfbXfsy9eY4xAaIYtbppE2DBzwrJu7us7Am1t4ofcQ==",
"path": "ltrdata.discutils.btrfs/1.0.28",
"hashPath": "ltrdata.discutils.btrfs.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Core/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-4DePAB6i7D8sejFDWbKVht15CZ6bRUIuICVdsOFtoBswE/HvUaPHcL9gsyj+UxQ1/hyacMdXKHsQq2S3qrCt0g==",
"path": "ltrdata.discutils.core/1.0.28",
"hashPath": "ltrdata.discutils.core.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Dmg/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ihK4qhJsNChG2rQ2EUzyvNwDqJCTN3MSwrNDwbrzZCoxkYHUHVztI1Qdc2we40fdz6ZG0areD/hbcrwtRRbgkQ==",
"path": "ltrdata.discutils.dmg/1.0.28",
"hashPath": "ltrdata.discutils.dmg.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Ext/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Gi8Mt1qrOzr04OFnZ0DRLRCtOrd+UE2DF3SSfTAAZHCNESYkdwCqJgpDN3J6atcq774vv8My8d5peybqkVIW8w==",
"path": "ltrdata.discutils.ext/1.0.28",
"hashPath": "ltrdata.discutils.ext.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Fat/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-wP/EUaDI6oDV8Hi45qznv/DMQ59b5Owb79MSdss335xHPA6qi6x0XjTkJ24f07hFPXUGxdMvq3mGH/YknALrVw==",
"path": "ltrdata.discutils.fat/1.0.28",
"hashPath": "ltrdata.discutils.fat.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.HfsPlus/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-uwGYBPXo+kqpAcA0EX7C+HrzZxzeCfFUjwBv6EcwcppnBF10ebr753Zx0OGZ/tz459ijhsybXZpOJHu1i50olg==",
"path": "ltrdata.discutils.hfsplus/1.0.28",
"hashPath": "ltrdata.discutils.hfsplus.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Iso9660/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-AaWwEub+uKnOXtW+K7XbYoItFUhdC8TLOdrhi91navA5estrPF+HInfFQ3NjshqwiLrNjAex1FU/IY9da5xRVQ==",
"path": "ltrdata.discutils.iso9660/1.0.28",
"hashPath": "ltrdata.discutils.iso9660.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Lvm/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-FTZxyHa2pNppVLIPdXTHROBVhFpzPuN6BOjvgNvPeFKwhG69+3P8ngdJ5CdqnZJl5SWDhq3m2AXQ1ztx8dNuRg==",
"path": "ltrdata.discutils.lvm/1.0.28",
"hashPath": "ltrdata.discutils.lvm.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Nfs/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-tFCJvrqYYYBeMrpgfzteAvgAzT26Sg0auobNoj6bfV5DGkSwkgrLBO2oPL/b7QRw3gG/Z9nLozw4iY7TSCKvfw==",
"path": "ltrdata.discutils.nfs/1.0.28",
"hashPath": "ltrdata.discutils.nfs.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Ntfs/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-FZ6UCnT335DQgGI9KB6jhGz0NGbduczTGIl5NxoLXe33ejlJHgh6jf9IKCvy3AkT9Nxl8HDEhTIbpU48V8c09g==",
"path": "ltrdata.discutils.ntfs/1.0.28",
"hashPath": "ltrdata.discutils.ntfs.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.OpticalDisk/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-LKOmjlBh9sj3lnCZZnO+4mcB7kbEnB0G+L00o78JDyt5zaEoVoVrMQ6vNLyCq5rm0Ermbm7yeR2wyTzb0hKVuQ==",
"path": "ltrdata.discutils.opticaldisk/1.0.28",
"hashPath": "ltrdata.discutils.opticaldisk.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Registry/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-NLzwxfS/eVri15blNVjGqQs/uga9eaem+Esh7h5bG6nAKZfee1bqV45mIHcMrmP2YYuGhPFRj4+Img75RK5iuA==",
"path": "ltrdata.discutils.registry/1.0.28",
"hashPath": "ltrdata.discutils.registry.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.SquashFs/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-RJpaZg0z7DIzK4bJCu2+qnemqGDaByChKO1ixZO24EsI2xLQzEjiF/2MK5k/Eoq7+jlFvJux/eTCrc2KSHYKRg==",
"path": "ltrdata.discutils.squashfs/1.0.28",
"hashPath": "ltrdata.discutils.squashfs.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Streams/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-x4MqHJqay5I13fF0DPNZz4NE3MOtRPf69wVcFxwMI5F58IPHPGC5jJPoumblCjHKOVCZoDE8UcVbpi0xmZ68bg==",
"path": "ltrdata.discutils.streams/1.0.28",
"hashPath": "ltrdata.discutils.streams.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Swap/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-7DfIy7uNRbQv4o5dZVbfnW/rLsSfdUroLPa/Cx1EAFNBKDbJOceHIoR1Eb1tEaXcaLSXVIAciLLNU2eBj70Pag==",
"path": "ltrdata.discutils.swap/1.0.28",
"hashPath": "ltrdata.discutils.swap.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Udf/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-WfU8WjXJygQU1t2dsbuxHh19dcTWwLrxSyHOL65pAF4UiuMdxgPH4eSmmNvmVC4un88DAuQta0hAjwsrOZkyfg==",
"path": "ltrdata.discutils.udf/1.0.28",
"hashPath": "ltrdata.discutils.udf.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Vdi/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-UeB8DwnMnDWFFaTdpXMN4QbFXNsx0XFujN+5i3Ua9Bb9h4beb+GJhyUPzw0uDy2qySBUhXk4SXbG50dA7+8NUg==",
"path": "ltrdata.discutils.vdi/1.0.28",
"hashPath": "ltrdata.discutils.vdi.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Vhd/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-DdJ4RtHL2bnXrSjJzAPRZDnBpUnaR0TGLqL1ETMw0N6E5/5HmzP3VNTuPklOWmmqpwdKBzC5tHzyCVKC78JWLA==",
"path": "ltrdata.discutils.vhd/1.0.28",
"hashPath": "ltrdata.discutils.vhd.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Vhdx/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-c/dg3XYOhG49kQT515vkeOpyBgvgYFOcH2YIiHhKIgBbgjcjVEgUHBTzMdhvW4yPPlEXRyOFRIG52rLUTyhsWA==",
"path": "ltrdata.discutils.vhdx/1.0.28",
"hashPath": "ltrdata.discutils.vhdx.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.VirtualFileSystem/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-1iXZ+fv80k57J8tjsJiXERP3M/8OQJqAxwmnYLRB7lX/zU5Z8loFSZJv/b7rT4+X/riUOG5FDIxPqjbLgACzAg==",
"path": "ltrdata.discutils.virtualfilesystem/1.0.28",
"hashPath": "ltrdata.discutils.virtualfilesystem.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Vmdk/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-eHtoRh9an72RSQbb6R2ud9xynRLVPHhlPIXueQblaQjRBD/snd/Csoo0M3l7vKSaHoYP3+r2wVlkz8Ap7sshsw==",
"path": "ltrdata.discutils.vmdk/1.0.28",
"hashPath": "ltrdata.discutils.vmdk.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Wim/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-oD0XtOLe3jlFcWwhaPBK5bD6TFcIJhB0G2HDj2bUN8JS9UohE4k77YAmRNsLSzgpD8Mu1guNbeV3M1poSmErXA==",
"path": "ltrdata.discutils.wim/1.0.28",
"hashPath": "ltrdata.discutils.wim.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Xfs/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-5ut6VvhTyMNaoJGdgNX89T2xgAB/WTAiDYVuUEA/bkTjj9xe4CnlbgbD08vkzQZ5zuM3X1Ab/HD1L0P7tTDo1A==",
"path": "ltrdata.discutils.xfs/1.0.28",
"hashPath": "ltrdata.discutils.xfs.1.0.28.nupkg.sha512"
},
"LTRData.DiscUtils.Xva/1.0.28": {
"type": "package",
"serviceable": true,
"sha512": "sha512-mgNTc7Zngzr30P2rVWSXU071fIJLkrOLuQbuiyP6dg17jt5n70h2XzGIbgZZDnHFP0XPMwjf2VHISzMYVylL8g==",
"path": "ltrdata.discutils.xva/1.0.28",
"hashPath": "ltrdata.discutils.xva.1.0.28.nupkg.sha512"
},
"LTRData.ExFat.Core/1.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-b6UUS/uH4Hd5RyUqfMMKbWwLniaP7BIBiynzmIfdkDIs6RO1rV5ZUvqU2RGepVmIsERM663vk6TbqEAfEdMqGg==",
"path": "ltrdata.exfat.core/1.0.10",
"hashPath": "ltrdata.exfat.core.1.0.10.nupkg.sha512"
},
"LTRData.ExFat.DiscUtils/1.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-mStJk3BrgOzdI8Ty9CKR1TtExC+IZ0y534CNJl3idP38I7Rxu/M79BZgnwjGJSK9Qazb3Oy7fRRgPtbcVCQNoA==",
"path": "ltrdata.exfat.discutils/1.0.10",
"hashPath": "ltrdata.exfat.discutils.1.0.10.nupkg.sha512"
},
"lzfse-net/1.0.15": {
"type": "package",
"serviceable": true,
"sha512": "sha512-O5AhPNxnNhI9Ena8b+VJ1hhgNac8rRmd1rnYopSEWHetU/1Z5xOFPuWT4nxpy2u5Qk7evS5kFj1dRDhcArBtpg==",
"path": "lzfse-net/1.0.15",
"hashPath": "lzfse-net.1.0.15.nupkg.sha512"
},
"lzo.net/0.0.6": {
"type": "package",
"serviceable": true,
"sha512": "sha512-d+F4Kf+Mnh3G83NOxXyxlZljkFvD5ZM7WUPu5Y7DTZousSgz1EAhpanSbj0+25KVrInroNoV/W9QqwtRH3avig==",
"path": "lzo.net/0.0.6",
"hashPath": "lzo.net.0.0.6.nupkg.sha512"
},
"Microsoft.Bcl.HashCode/1.1.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-MalY0Y/uM/LjXtHfX/26l2VtN4LDNZ2OE3aumNOHDLsT4fNYy2hiHXI4CXCqKpNUNm7iJ2brrc4J89UdaL56FA==",
"path": "microsoft.bcl.hashcode/1.1.1",
"hashPath": "microsoft.bcl.hashcode.1.1.1.nupkg.sha512"
},
"Microsoft.Management.Infrastructure/2.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-IaKZRNBBv3sdrmBWd+aqwHq8cVHk/3WgWFAN/dt40MRY9rbtHiDfTTmaEN0tGTmQqGCGDo/ncntA8MvFMvcsRw==",
"path": "microsoft.management.infrastructure/2.0.0",
"hashPath": "microsoft.management.infrastructure.2.0.0.nupkg.sha512"
},
"Microsoft.Management.Infrastructure.Runtime.Unix/2.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-p0lslMX5bdWLxO2P7ao+rjAMOB0LEwPYpzvdCQ2OEYgX2NxFpQ8ILvqPGnYlTAb53rT8gu5DyIol1HboHFYfxQ==",
"path": "microsoft.management.infrastructure.runtime.unix/2.0.0",
"hashPath": "microsoft.management.infrastructure.runtime.unix.2.0.0.nupkg.sha512"
},
"Microsoft.Management.Infrastructure.Runtime.Win/2.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-vjBWQeDOjgernkrOdbEgn7M70SF7hof7ORdKPSlL06Uc15+oYdth5dZju9KsgUoti/cwnkZTiwtDx/lRtay0sA==",
"path": "microsoft.management.infrastructure.runtime.win/2.0.0",
"hashPath": "microsoft.management.infrastructure.runtime.win.2.0.0.nupkg.sha512"
},
"Microsoft.NETCore.Platforms/5.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ==",
"path": "microsoft.netcore.platforms/5.0.0",
"hashPath": "microsoft.netcore.platforms.5.0.0.nupkg.sha512"
},
"Microsoft.NETCore.Targets/1.1.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==",
"path": "microsoft.netcore.targets/1.1.0",
"hashPath": "microsoft.netcore.targets.1.1.0.nupkg.sha512"
},
"Microsoft.Win32.Registry/5.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==",
"path": "microsoft.win32.registry/5.0.0",
"hashPath": "microsoft.win32.registry.5.0.0.nupkg.sha512"
},
"Microsoft.Win32.SystemEvents/4.7.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-mtVirZr++rq+XCDITMUdnETD59XoeMxSpLRIII7JRI6Yj0LEDiO1pPn0ktlnIj12Ix8bfvQqQDMMIF9wC98oCA==",
"path": "microsoft.win32.systemevents/4.7.0",
"hashPath": "microsoft.win32.systemevents.4.7.0.nupkg.sha512"
},
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-7VSGO0URRKoMEAq0Sc9cRz8mb6zbyx/BZDEWhgPdzzpmFhkam3fJ1DAGWFXBI4nGlma+uPKpfuMQP5LXRnOH5g==",
"path": "runtime.debian.8-x64.runtime.native.system.security.cryptography.openssl/4.3.2",
"hashPath": "runtime.debian.8-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512"
},
"runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-0oAaTAm6e2oVH+/Zttt0cuhGaePQYKII1dY8iaqP7CvOpVKgLybKRFvQjXR2LtxXOXTVPNv14j0ot8uV+HrUmw==",
"path": "runtime.fedora.23-x64.runtime.native.system.security.cryptography.openssl/4.3.2",
"hashPath": "runtime.fedora.23-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512"
},
"runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-G24ibsCNi5Kbz0oXWynBoRgtGvsw5ZSVEWjv13/KiCAM8C6wz9zzcCniMeQFIkJ2tasjo2kXlvlBZhplL51kGg==",
"path": "runtime.fedora.24-x64.runtime.native.system.security.cryptography.openssl/4.3.2",
"hashPath": "runtime.fedora.24-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512"
},
"runtime.native.System.Security.Cryptography.Apple/4.3.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-UPrVPlqPRSVZaB4ADmbsQ77KXn9ORiWXyA1RP2W2+byCh3bhgT1bQz0jbeOoog9/2oTQ5wWZSDSMeb74MjezcA==",
"path": "runtime.native.system.security.cryptography.apple/4.3.1",
"hashPath": "runtime.native.system.security.cryptography.apple.4.3.1.nupkg.sha512"
},
"runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-QR1OwtwehHxSeQvZKXe+iSd+d3XZNkEcuWMFYa2i0aG1l+lR739HPicKMlTbJst3spmeekDVBUS7SeS26s4U/g==",
"path": "runtime.native.system.security.cryptography.openssl/4.3.2",
"hashPath": "runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512"
},
"runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-I+GNKGg2xCHueRd1m9PzeEW7WLbNNLznmTuEi8/vZX71HudUbx1UTwlGkiwMri7JLl8hGaIAWnA/GONhu+LOyQ==",
"path": "runtime.opensuse.13.2-x64.runtime.native.system.security.cryptography.openssl/4.3.2",
"hashPath": "runtime.opensuse.13.2-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512"
},
"runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-1Z3TAq1ytS1IBRtPXJvEUZdVsfWfeNEhBkbiOCGEl9wwAfsjP2lz3ZFDx5tq8p60/EqbS0HItG5piHuB71RjoA==",
"path": "runtime.opensuse.42.1-x64.runtime.native.system.security.cryptography.openssl/4.3.2",
"hashPath": "runtime.opensuse.42.1-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512"
},
"runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple/4.3.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-t15yGf5r6vMV1rB5O6TgfXKChtCaN3niwFw44M2ImX3eZ8yzueplqMqXPCbWzoBDHJVz9fE+9LFUGCsUmS2Jgg==",
"path": "runtime.osx.10.10-x64.runtime.native.system.security.cryptography.apple/4.3.1",
"hashPath": "runtime.osx.10.10-x64.runtime.native.system.security.cryptography.apple.4.3.1.nupkg.sha512"
},
"runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-6mU/cVmmHtQiDXhnzUImxIcDL48GbTk+TsptXyJA+MIOG9LRjPoAQC/qBFB7X+UNyK86bmvGwC8t+M66wsYC8w==",
"path": "runtime.osx.10.10-x64.runtime.native.system.security.cryptography.openssl/4.3.2",
"hashPath": "runtime.osx.10.10-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512"
},
"runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-vjwG0GGcTW/PPg6KVud8F9GLWYuAV1rrw1BKAqY0oh4jcUqg15oYF1+qkGR2x2ZHM4DQnWKQ7cJgYbfncz/lYg==",
"path": "runtime.rhel.7-x64.runtime.native.system.security.cryptography.openssl/4.3.2",
"hashPath": "runtime.rhel.7-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512"
},
"runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-7KMFpTkHC/zoExs+PwP8jDCWcrK9H6L7soowT80CUx3e+nxP/AFnq0AQAW5W76z2WYbLAYCRyPfwYFG6zkvQRw==",
"path": "runtime.ubuntu.14.04-x64.runtime.native.system.security.cryptography.openssl/4.3.2",
"hashPath": "runtime.ubuntu.14.04-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512"
},
"runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-xrlmRCnKZJLHxyyLIqkZjNXqgxnKdZxfItrPkjI+6pkRo5lHX8YvSZlWrSI5AVwLMi4HbNWP7064hcAWeZKp5w==",
"path": "runtime.ubuntu.16.04-x64.runtime.native.system.security.cryptography.openssl/4.3.2",
"hashPath": "runtime.ubuntu.16.04-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512"
},
"runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl/4.3.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-leXiwfiIkW7Gmn7cgnNcdtNAU70SjmKW3jxGj1iKHOvdn0zRWsgv/l2OJUO5zdGdiv2VRFnAsxxhDgMzofPdWg==",
"path": "runtime.ubuntu.16.10-x64.runtime.native.system.security.cryptography.openssl/4.3.2",
"hashPath": "runtime.ubuntu.16.10-x64.runtime.native.system.security.cryptography.openssl.4.3.2.nupkg.sha512"
},
"System.Buffers/4.5.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==",
"path": "system.buffers/4.5.1",
"hashPath": "system.buffers.4.5.1.nupkg.sha512"
},
"System.CodeDom/4.7.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Hs9pw/kmvH3lXaZ1LFKj3pLQsiGfj2xo3sxSzwiLlRL6UcMZUTeCfoJ9Udalvn3yq5dLlPEZzYegrTQ1/LhPOQ==",
"path": "system.codedom/4.7.0",
"hashPath": "system.codedom.4.7.0.nupkg.sha512"
},
"System.Collections/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==",
"path": "system.collections/4.3.0",
"hashPath": "system.collections.4.3.0.nupkg.sha512"
},
"System.Collections.Concurrent/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==",
"path": "system.collections.concurrent/4.3.0",
"hashPath": "system.collections.concurrent.4.3.0.nupkg.sha512"
},
"System.Diagnostics.Debug/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==",
"path": "system.diagnostics.debug/4.3.0",
"hashPath": "system.diagnostics.debug.4.3.0.nupkg.sha512"
},
"System.Diagnostics.EventLog/7.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-eUDP47obqQm3SFJfP6z+Fx2nJ4KKTQbXB4Q9Uesnzw9SbYdhjyoGXuvDn/gEmFY6N5Z3bFFbpAQGA7m6hrYJCw==",
"path": "system.diagnostics.eventlog/7.0.0",
"hashPath": "system.diagnostics.eventlog.7.0.0.nupkg.sha512"
},
"System.Diagnostics.Tracing/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==",
"path": "system.diagnostics.tracing/4.3.0",
"hashPath": "system.diagnostics.tracing.4.3.0.nupkg.sha512"
},
"System.Drawing.Common/4.7.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-v+XbyYHaZjDfn0ENmJEV1VYLgGgCTx1gnfOBcppowbpOAriglYgGCvFCPr2EEZyBvXlpxbEsTwkOlInl107ahA==",
"path": "system.drawing.common/4.7.0",
"hashPath": "system.drawing.common.4.7.0.nupkg.sha512"
},
"System.Globalization/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==",
"path": "system.globalization/4.3.0",
"hashPath": "system.globalization.4.3.0.nupkg.sha512"
},
"System.IO/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==",
"path": "system.io/4.3.0",
"hashPath": "system.io.4.3.0.nupkg.sha512"
},
"System.IO.FileSystem.AccessControl/5.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-SxHB3nuNrpptVk+vZ/F+7OHEpoHUIKKMl02bUmYHQr1r+glbZQxs7pRtsf4ENO29TVm2TH3AEeep2fJcy92oYw==",
"path": "system.io.filesystem.accesscontrol/5.0.0",
"hashPath": "system.io.filesystem.accesscontrol.5.0.0.nupkg.sha512"
},
"System.Linq/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==",
"path": "system.linq/4.3.0",
"hashPath": "system.linq.4.3.0.nupkg.sha512"
},
"System.Management/4.7.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-IY+uuGhgzWiCg21i8IvQeY/Z7m1tX8VuPF+ludfn7iTCaccTtJo5HkjZbBEL8kbBubKhAKKtNXr7uMtmAc28Pw==",
"path": "system.management/4.7.0",
"hashPath": "system.management.4.7.0.nupkg.sha512"
},
"System.Memory/4.5.5": {
"type": "package",
"serviceable": true,
"sha512": "sha512-XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
"path": "system.memory/4.5.5",
"hashPath": "system.memory.4.5.5.nupkg.sha512"
},
"System.Reflection/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==",
"path": "system.reflection/4.3.0",
"hashPath": "system.reflection.4.3.0.nupkg.sha512"
},
"System.Reflection.Primitives/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==",
"path": "system.reflection.primitives/4.3.0",
"hashPath": "system.reflection.primitives.4.3.0.nupkg.sha512"
},
"System.Resources.Extensions/7.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-65ufm9ABXvxRkQ//hMcUDrQXbGWkC7z0WWZAvHlQ6Qv+JmrIwHH1lmX8aXlNlXpIrT9KxDpuZPqJTVqqwzMD8Q==",
"path": "system.resources.extensions/7.0.0",
"hashPath": "system.resources.extensions.7.0.0.nupkg.sha512"
},
"System.Resources.ResourceManager/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==",
"path": "system.resources.resourcemanager/4.3.0",
"hashPath": "system.resources.resourcemanager.4.3.0.nupkg.sha512"
},
"System.Runtime/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
"path": "system.runtime/4.3.0",
"hashPath": "system.runtime.4.3.0.nupkg.sha512"
},
"System.Runtime.CompilerServices.Unsafe/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==",
"path": "system.runtime.compilerservices.unsafe/6.0.0",
"hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512"
},
"System.Runtime.Extensions/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==",
"path": "system.runtime.extensions/4.3.0",
"hashPath": "system.runtime.extensions.4.3.0.nupkg.sha512"
},
"System.Runtime.Handles/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==",
"path": "system.runtime.handles/4.3.0",
"hashPath": "system.runtime.handles.4.3.0.nupkg.sha512"
},
"System.Runtime.InteropServices/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==",
"path": "system.runtime.interopservices/4.3.0",
"hashPath": "system.runtime.interopservices.4.3.0.nupkg.sha512"
},
"System.Runtime.Numerics/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==",
"path": "system.runtime.numerics/4.3.0",
"hashPath": "system.runtime.numerics.4.3.0.nupkg.sha512"
},
"System.Security.AccessControl/5.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==",
"path": "system.security.accesscontrol/5.0.0",
"hashPath": "system.security.accesscontrol.5.0.0.nupkg.sha512"
},
"System.Security.Cryptography.Algorithms/4.3.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-DVUblnRfnarrI5olEC2B/OCsJQd0anjVaObQMndHSc43efbc88/RMOlDyg/EyY0ix5ecyZMXS8zMksb5ukebZA==",
"path": "system.security.cryptography.algorithms/4.3.1",
"hashPath": "system.security.cryptography.algorithms.4.3.1.nupkg.sha512"
},
"System.Security.Cryptography.Encoding/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==",
"path": "system.security.cryptography.encoding/4.3.0",
"hashPath": "system.security.cryptography.encoding.4.3.0.nupkg.sha512"
},
"System.Security.Cryptography.Primitives/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==",
"path": "system.security.cryptography.primitives/4.3.0",
"hashPath": "system.security.cryptography.primitives.4.3.0.nupkg.sha512"
},
"System.Security.Permissions/4.7.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-dkOV6YYVBnYRa15/yv004eCGRBVADXw8qRbbNiCn/XpdJSUXkkUeIvdvFHkvnko4CdKMqG8yRHC4ox83LSlMsQ==",
"path": "system.security.permissions/4.7.0",
"hashPath": "system.security.permissions.4.7.0.nupkg.sha512"
},
"System.Security.Principal.Windows/5.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==",
"path": "system.security.principal.windows/5.0.0",
"hashPath": "system.security.principal.windows.5.0.0.nupkg.sha512"
},
"System.ServiceProcess.ServiceController/7.0.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-rPfXTJzYU46AmWYXRATQzQQ01hICrkl3GuUHgpAr9mnUwAVSsga5x3mBxanFPlJBV9ilzqMXbQyDLJQAbyTnSw==",
"path": "system.serviceprocess.servicecontroller/7.0.1",
"hashPath": "system.serviceprocess.servicecontroller.7.0.1.nupkg.sha512"
},
"System.Text.Encoding/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==",
"path": "system.text.encoding/4.3.0",
"hashPath": "system.text.encoding.4.3.0.nupkg.sha512"
},
"System.Text.Encoding.CodePages/7.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==",
"path": "system.text.encoding.codepages/7.0.0",
"hashPath": "system.text.encoding.codepages.7.0.0.nupkg.sha512"
},
"System.Text.Encodings.Web/7.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-OP6umVGxc0Z0MvZQBVigj4/U31Pw72ITihDWP9WiWDm+q5aoe0GaJivsfYGq53o6dxH7DcXWiCTl7+0o2CGdmg==",
"path": "system.text.encodings.web/7.0.0",
"hashPath": "system.text.encodings.web.7.0.0.nupkg.sha512"
},
"System.Text.Json/7.0.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-AyjhwXN1zTFeIibHimfJn6eAsZ7rTBib79JQpzg8WAuR/HKDu9JGNHTuu3nbbXQ/bgI+U4z6HtZmCHNXB1QXrQ==",
"path": "system.text.json/7.0.3",
"hashPath": "system.text.json.7.0.3.nupkg.sha512"
},
"System.Threading/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==",
"path": "system.threading/4.3.0",
"hashPath": "system.threading.4.3.0.nupkg.sha512"
},
"System.Threading.Tasks/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==",
"path": "system.threading.tasks/4.3.0",
"hashPath": "system.threading.tasks.4.3.0.nupkg.sha512"
},
"System.Threading.Tasks.Extensions/4.5.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==",
"path": "system.threading.tasks.extensions/4.5.4",
"hashPath": "system.threading.tasks.extensions.4.5.4.nupkg.sha512"
},
"System.ValueTuple/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==",
"path": "system.valuetuple/4.5.0",
"hashPath": "system.valuetuple.4.5.0.nupkg.sha512"
},
"System.Windows.Extensions/4.7.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-CeWTdRNfRaSh0pm2gDTJFwVaXfTq6Xwv/sA887iwPTneW7oMtMlpvDIO+U60+3GWTB7Aom6oQwv5VZVUhQRdPQ==",
"path": "system.windows.extensions/4.7.0",
"hashPath": "system.windows.extensions.4.7.0.nupkg.sha512"
},
"Microsoft.Win32.TaskScheduler/2.9.1.0": {
"type": "reference",
"serviceable": false,
"sha512": ""
}
}
}
================================================
FILE: tools/Arsenal-Image-Mounter-v3.10.257/ArsenalImageMounter.log
================================================
---------------
2023-09-06 14:42:21
Starting up: Application 'C:\Users\Radi\Desktop\Final_year_project\Arsenal-Image-Mounter-v3.10.257\ArsenalImageMounter.dll' version 3.10.257. OS = 'Microsoft Windows 10.0.19045 X64'. Framework = .NET 6.0.14 X64
---------------
2023-09-06 14:42:21
Licensed mode = False
---------------
2023-09-06 14:42:48
Image file 'C:\Users\Radi\Desktop\2020JimmyWilson.E01', detected GuidPartitionTable, 2 partitions.
0x10080 - 0x1A807F (Windows Basic Data), detected 'Microsoft NTFS' (healthy).
---------------
2023-09-06 14:43:36
Exit: Application 'C:\Users\Radi\Desktop\Final_year_project\Arsenal-Image-Mounter-v3.10.257\ArsenalImageMounter.dll' version 3.10.257. OS = 'Microsoft Windows 10.0.19045 X64'. Framework = .NET 6.0.14 X64
================================================
FILE: tools/Arsenal-Image-Mounter-v3.10.257/ArsenalImageMounter.runtimeconfig.json
================================================
{
"runtimeOptions": {
"tfm": "net6.0",
"frameworks": [
{
"name": "Microsoft.NETCore.App",
"version": "6.0.0"
},
{
"name": "Microsoft.WindowsDesktop.App",
"version": "6.0.0"
}
],
"configProperties": {
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false
}
}
}
================================================
FILE: tools/Arsenal-Image-Mounter-v3.10.257/aim_cli.runtimeconfig.json
================================================
{
"runtimeOptions": {
"tfm": "net6.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "6.0.0"
},
"configProperties": {
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false
}
}
}
================================================
FILE: tools/Arsenal-Image-Mounter-v3.10.257/readme.txt
================================================
Please read "Arsenal Recon - End User License Agreement.txt" carefully before using this software.
Arsenal Image Mounter
Many Windows®-based disk image mounting solutions mount the contents of disk images as shares or partitions, rather than complete (a/k/a “physical” or “real”) disks, which limits their usefulness to digital forensics practitioners and others. Arsenal Image Mounter mounts the contents of disk images as complete disks in Windows. As far as Windows is concerned, the contents of disk images mounted by Arsenal Image Mounter are real SCSI disks, allowing users to benefit from disk-specific features like integration with Disk Manager, launching virtual machines (and then bypassing Windows authentication and DPAPI), managing BitLocker-protected volumes, mounting Volume Shadow Copies, and more.
End Users: If Arsenal Image Mounter is run without a license, it will run in "Free Mode" and provide core functionality. If Arsenal Image Mounter is licensed, it will run in "Professional Mode” with full functionality enabled.
Feature highlights:
[Free Mode] Mount raw, forensic, and virtual machine disk images as complete (a/k/a “real”) disks on Windows
[Free Mode] Temporary write support with replayable differencing files for all supported disk image formats
[Free Mode] Save "physically" mounted objects to various disk image formats
[Free Mode] Identify (with details), unlock, fully decrypt, and disable/suspend BitLocker-protected volumes
[Free Mode] Access disks, volumes, and Volume Shadow Copies as virtual dd files
[Free Mode] Virtually mount optical images
[Free Mode] RAM disk creation with either static or dynamic memory allocation
[Free Mode] Command-line interface (CLI) executables
[Free Mode] MBR injection, fake disk signatures, removable disk emulation, and much more
[Professional Mode] Effortlessly launch virtual machines from disk images
[Professional Mode] Extremely powerful Windows authentication and DPAPI bypasses within virtual machines
[Professional Mode] Volume Shadow Copy mounting (standard, with Windows NTFS driver bypass, or as complete disks)
[Professional Mode] Launch virtual machines directly from Volume Shadow Copies
[Professional Mode] Attach to actual physical disks (fixed and removable) to leverage virtual machine launching, VSC mounting, etc.
[Professional Mode] Write mounted disk images to physical disks with optional free space clearing
[Professional Mode] Windows file system driver bypass (FAT, NTFS, ExFAT, HFS+, Ext2/3/4, etc.)
[Professional Mode] Exposure of NTFS metadata, slack, and unallocated in Windows file system driver bypass mode
[Professional Mode] Virtually mount archives and directories
[Professional Mode] Save disk images with fully-decrypted BitLocker volumes
Developers: Arsenal Image Mounter source code, APIs, and executables are available for royalty-free use in open source projects. Commercial projects (and other projects not licensed under an AGPL v3 compatible license) that would like to use Arsenal Image Mounter source code, APIs, and/or executables must contact Arsenal (sales@ArsenalRecon.com) to obtain alternative licensing.
Please Note: We recommend excluding/whitelisting Arsenal Image Mounter's folder and/or executables (ArsenalImageMounter.exe and aim_cli.exe) in your anti-virus software, as we have encountered situations in which anti-virus software has prevented (sometimes silently) Arsenal Image Mounter from launching successfully. When launching virtual machines with AIM, you may need to temporarily disable real-time protection and/or instruct your anti-virus software to ignore/allow the "utilman.exe" threat, to ensure that AIM Virtual Machine Tools will be injected into the VMs properly. In some rare cases you may need to disable your anti-virus software entirely, as we have found one anti-virus product that interferes with AIM even after proper whitelisting.
Detailed feature descriptions (Mount options):
Disk device, read only - Mount the disk image as a read-only disk device. No write operations will be allowed. Please note, when mounting a disk image containing a partition rather than a complete disk as a disk device in read only, write temporary, and write original modes, AIM will inject what is necessary (for example, a fake MBR) so that Windows will be able to properly mount the disk image as a complete disk. The disk image itself will not be modified.
Please note: Arsenal does not recommend mounting VSCs after mounting disk images in read-only mode. See the FAQ entry "Can you provide more detail about the Windows bugs identified by Maxim Suhanov and Arsenal which sometimes result in "missing" Volume Shadow Copies?" for more information.
Disk device, write temporary - Mount the disk image as a writable disk device using the AIM write filter. Modifications will be written to a write-overlay differencing file and the original disk image will not be changed. Sometimes referred to as write-overlay or write-copy mode. (Note - required for launching virtual machines.) If you would like to choose an alternate location for the differencing file, check the "Specify an alternate differential file location" box. AIM will also ask for an alternate differential file location if the disk image you are about to mount is already open one or more times in this mode, or if writing to the same location as the disk image is not possible because the location in which the disk image is located is write protected. If you would like AIM to delete the differencing file after the disk image is unmounted, check the "Delete differencing file after unmount" box. This option is disabled when the "Automatically remount at Arsenal Image Mounter startup" box is checked. You can choose to store the differencing data within host RAM only, and not in a file, if you check the "Store differencing data in host RAM only (not in a file)" box - but please note, a significant amount of RAM may be required (Arsenal recommends your host have at least 32GB RAM, and at least 16GB of free physical memory when launching virtual machines) especially in extreme situations such as fully decrypting BitLocker-protected volumes.
Windows file system driver bypass, read only - Mount the disk image as a virtual read-only file system, using DiscUtils rather than Windows file system drivers. This mount option is often used to bypass file system security, expose NTFS metafiles and streams, and recover deleted files. May also be useful to read files from disk images containing corrupted file systems. (Note - BitLocker-protected volumes and VSC mounting are not supported, and disk size values are an approximation of each volume's total file size (including things like multiple links to the same file and files with sparse allocation) so the size may appear larger than the expected volume size.) Single disk, non-striped, lvm/lvm2 volumes are supported. Please note that using Windows File Explorer to copy items from this mount mode may result in unexpected behavior (especially when dealing with file systems other than NTFS and FAT) because of limitations which other tools designed to copy entire folder trees (such as robocopy) do not have - for example, Windows File Explorer will silently stop copying when hitting folders named after reserved device names like "aux", "con", etc. Also, be aware of things like symbolic links and case sensitivity (again, especially when dealing with file systems other than NTFS and FAT) which may cause unexpected behavior as you copy items from this mount mode. Finally, make sure to review the FAQ "Can you describe some of the NTFS-related things exposed by the Windows file system driver bypass mount option as well as any NTFS-related limitations?" below when mounting NTFS file systems.
Disk device, write original - Mount the disk image as a writable disk device. Caution, modifications will be written to the original disk image.
Windows file system driver bypass, write original - Mount the disk image as a virtual writable file system. Caution, modifications will be written to the original disk image. This mount option bypasses file system security but does not expose most NTFS metafiles and streams. (Note - BitLocker-protected volumes and VSC mounting are not supported, and disk size values are an approximation of each volume's total file size (including things like multiple links to the same file and files with sparse allocation) so the size may appear larger than the expected volume size.) Single disk, non-striped, lvm/lvm2 volumes are supported. Please note that using Windows File Explorer to copy items from this mount mode may result in unexpected behavior (especially when dealing with file systems other than NTFS and FAT) because of limitations which other tools designed to copy entire folder trees (such as robocopy) do not have - for example, Windows File Explorer will silently stop copying when hitting folders named after reserved device names like "aux", "con", etc. Also, be aware of things like symbolic links and case sensitivity (again, especially when dealing with file systems other than NTFS and FAT) which may cause unexpected behavior as you copy items from this mount mode.
Please note: In regard to the aforementioned mount modes - Arsenal Image Mounter’s core functionality involves mounting disk images as “real” disks on Windows, so in this sense, it is similar to attaching an actual physical disk to Windows. An important thing to be aware of, especially if using Windows Explorer to navigate AIM-mounted disk images, is behavior consistent with an actual physical disk attachment such as the Recycle Bin displaying information from all mounted volumes, junction and symbolic link redirections, and other behaviors associated with using Explorer to navigate an actual physical disk.
Sector size - Arsenal Image Mounter will normally select the correct sector size by default, based on disk image metadata, but there are situations in which it may need to be set manually. For example, you may need to set a sector size manually if the sector size is unusual and you are dealing with a raw disk image (without metadata) or the sector size specified in disk image metadata is incorrect.
Fake disk signature - Report a random disk signature to Windows. Useful if the disk image contains a zeroed-out disk signature or you are attempting to mount a duplicate disk signature. (Note - requires a valid MBR and partition table. Not compatible with GPT partitions or images without a partition table.)
Create “removable” disk device - Emulate the attachment of a USB thumb drive, which may facilitate the successful mounting of disk images containing partitions rather than complete disks or disk images without partition tables. (Caution - see relevant FAQ for caveats.)
Automatically remount at Arsenal Image Mounter startup - Remount the disk image with the current mount options when AIM next starts. To retain this persistent mounting, simply quit AIM with the disk image mounted. To cancel this persistent mounting, use "Remove" or "Remove all" before quitting AIM.
Detailed feature descriptions (Main screen and dropdown menus):
Mount VSCs - Mount Volume Shadow Copies (VSCs) within a disk image*1. Hidden/offline VSCs*2 will be made available (a/k/a forced online) if desired, by modifying VSC metadata on disk while the disk image is mounted in write-temporary mode*3. A list of VSCs is provided along with timestamps which allows any combination of VSCs to be mounted. The following options are available:
• Standard Volume Shadow Copy mount - Mount contents of VSCs using Windows NTFS driver. This option is fast, but does not expose file system metafiles and does not bypass file system security.
• Volume Shadow Copy mount with Windows file system driver bypass - Mount contents of VSCs using DiscUtils NTFS driver. Useful to expose NTFS metafiles and to bypass file system security.
• Write temporary Volume Shadow Copy mount - Mount contents of VSCs as complete disks in write-temporary mode using Windows NTFS driver. This option is useful for launching VSCs into virtual machines, but does not expose file system metafiles and does not bypass file system security. Please note, the stability of virtual machines launched from VSCs can be impacted by things like dirty filesystems and missing clusters - for example, applications may not launch as expected.
*1 AIM uses Windows to interact with VSCs. It is important to be aware that Windows does not reserve space within VSCs for certain files (Registry keys to be aware of include HKLM\System\CurrentControlSet\Control\BackupRestore\FilesNotToBackup and FilesNotToSnapshot) which can be confusing because those files appear to exist based on file system metadata. Also, data can be (and often is) missing or otherwise corrupt in VSCs due to both known (for example, the aforementioned Registry keys and live imaging) and unknown factors - keep this in mind to appropriately set expectations in terms of VSC contents.
*2 Including those created by the host (for example, per chkdsk activity) while a disk image has been mounted in write temporary or write-original modes. AIM will provide a warning about these kinds of hidden/offline VSCs, because mounting them may result in unpredictable behavior such as deadlocks.
*3 Onlining hidden/offline VSCs is not supported in read only mode, only write temporary and write original modes. If AIM identifies hidden/offline VSCs within encrypted volumes, even while in write-temporary mode, those volumes must be decrypted so that AIM can then modify VSC metadata and make them available.
Launch VM – Launch a Hyper-V virtual machine using the selected AIM-mounted (or attached) disk. The disk image (or actual physical disk) should be mounted in write-temporary mode before using this feature, which is designed to make booting the contents of a disk in a virtual machine more efficient, reliable, and useful than other methods. Just after selecting Launch VM, AIM will perform file system scanning (which may take some time) in order to identify dirty file systems, avoid unexpected locks, and deal with other possible issues with launching the selected disk into a VM. AIM will determine whether the disk should be launched as a Generation 1 or Generation 2 virtual machine - see https://docs.microsoft.com/en-us/windows-server/virtualization/hyper-v/plan/should-i-create-a-generation-1-or-2-virtual-machine-in-hyper-v for more details about the two Generations. The virtual machine is created with half of the available physical CPU cores, half of free host RAM (maximum of 6GB if >10GB is available), two network adapters (not connected by default), one DVD-ROM (without any attached image) and the AIM-mounted/attached disk as the primary IDE or SCSI HD. The Launch VM feature currently works (with full functionality) on Windows 10/11 (and Server 2016/2019) x64 and requires that Hyper-V role be running on physical hardware, not within a virtual machine. Information from Microsoft about installing Hyper-V on Windows 10 is available at https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v. Arsenal's preference in terms of installing Hyper-V is the "Enable Hyper-V with CMD and DISM" method. If you are unsure whether Hyper-V is running, the output from "sc query HvService" at a command prompt can be helpful. Please note that hibernation files are not applied when disks are launched into virtual machines, because the same physical hardware would be required for hibernation to be applied properly - so use tools like Hibernation Recon to assist in the analysis of populated hibernation files.
Upon selecting Launch VM, AIM offers various options related to launching the virtual machine. You can choose to:
• Configure the network connection so that it is disconnected (i.e. completely isolated), shared between VMs, shared between VMs and host, or set to the default switch with external NAT.
• Enable guest services to provide copy/paste and other functionality between the VM and host. This is not recommended when isolation between the VM and host is preferred.
• Check file systems and boot environment, repairing and adjusting as necessary. If this option is available (not greyed out), Arsenal recommends that you not deselect it.
• Disable/suspend BitLocker-protected volumes, so that they do not need to be unlocked again once the VM is running.
• Inject AIM Virtual Machine Tools and adjust boot drivers. Some disks can be difficult to boot directly into virtual machines, so AIM can inject*1 a small application into virtual machines running Windows*2. AIM Virtual Machine Tools displays a list of accounts (accounts without tangible folder structure or without crucial Registry information are not listed in AIM Virtual Machine Tools, as there would effectively be nothing to login to and/or no way to do so) and can open an administrative command prompt from the login screen. AIM Virtual Machine Tools will launch automatically on Windows XP and can be accessed via the “Ease of Access” or "Accessibility" icon on Windows Vista/7/8/8.1/10/11*3. Please note, the list of accounts displayed by AIM Virtual Machine Tools will sometimes include recovered passwords which are particularly important in certain situations (involving Windows 8/8.1) in which automatic logons are not available and manual logons must be performed to unlock DPAPI.
• Boot with last Windows shutdown time (and disable Hyper-V's automatic time updates), which is useful (for example) to avoid time-based automatic cleanup functions (such as browser history cleanup) and to deal with time-based software licensing. The last shutdown time will be displayed in UTC. Please note, the last shutdown time comes from the Registry value ShutdownTime at HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Windows, so (for example) a system which crashed and did not properly shutdown or a disk image obtained live may not have the (initially) expected last shutdown time.
• Bypass Windows authentication*4 using a variety of techniques, including policy adjustments, so that any password input will work. Local, Microsoft (cloud), Active Directory, and Azure Active Directory accounts, using many kinds of authentication including passwords, PINs, biometrics, images, and smart cards, are supported. Accounts will be provided with administrative privileges if requested or if the original privileges are unavailable. Please note that to access certain things in Windows like EFS-encrypted items and cached login credentials you will need to crack, and not bypass, account authentication... or perform a combination of Windows authentication and DPAPI bypasses*5. Also - if you already have credentials for a Windows account, Arsenal recommends disabling this option so (for example) if you mistype a user's password you will be alerted to that fact rather than logged in anyway.
• Bypass Data Protection API (DPAPI), which provides seamless access (particularly in concert with AIM's Windows authentication bypass) to the selected user's DPAPI-protected content such as website, network share, and application credentials as well as files and folders protected by Encrypting File System (EFS). DPAPI-protected content is normally made available after a user successfully logs into Windows, but AIM's DPAPI bypass makes it available without already having the user's credentials. AIM offers three DPAPI bypass modes:
1.) Quick - This is an immediate DPAPI bypass and will be available in certain situations when AIM first launches a Windows system with local, Microsoft (cloud), and/or Azure AD accounts into a virtual machine. If AIM determines that one or more users are eligible for a quick DPAPI bypass, they will be shown in the dropdown list appended with (Quick). Please note, a quick DPAPI bypass is most effective with the last logged-on user on Windows 10 and 11 x64 (but will sometimes be available for previously logged-on users and earlier Windows versions as well), it does not persist across reboots, and in certain situations (involving Windows 8/8.1) an automatic logon is not possible even though AIM has recovered a user's password (resulting in booting to the Windows logon screen rather than the selected user's Desktop) - so the recovered password will be displayed by AIM Virtual Machine Tools for use in a manual logon, thereby unlocking DPAPI.
2.) PIN - AIM has already recovered a numeric PIN (in the brief amount of time since selecting the Launch VM button) for the associated user and displays it both in the dropdown list and within AIM Virtual Machine Tools. Once at the Windows logon screen, this PIN can be used for a manual logon, thereby unlocking DPAPI. Please note, if a Quick DPAPI bypass is also available for the associated user, AIM may use the Quick DPAPI bypass anyway and is displaying this already recovered PIN for informational purposes. Also, keep in mind that whether a PIN is found or needs to be brute forced is not only based on workstation speed but on environmental factors such as file system scanning - it is possible that a PIN is found on the first launch into a VM, but on subsequent launches (in the same Windows session) a brute force is required.
3.) PIN Brute Force - AIM has determined that a brute force of the associated user’s numeric PIN is possible, and will provide an ETA for brute forcing after this dropdown item is selected. AIM will provide the recovered PIN within AIM Virtual Machine Tools so that it can be used for a manual logon, thereby unlocking DPAPI. Please note, if a Quick DPAPI bypass is also available for the associated user, AIM may use the Quick DPAPI bypass anyway and then the recovered PIN can be viewed by launching AIM Virtual Machine Tools within the VM by using Start/Run and then running “aimvm”.
• Bypass Linux authentication. AIM will attempt to bypass Linux authentication for all users so that logons (whether typical or text-based via Ctrl+Alt+F2) are possible without passwords.
• Advanced: Enable nested virtualization. Leverage Hyper-V's nested virtualization which is useful (for example) when using Windows Subsystem for Android within the virtual machine launched by AIM. Keep in mind the Hyper-V nested virtualization requirements: on an Intel processor with VT-x and EPT technology, the Hyper-V host must be Windows Server 2016/Windows 10 or greater and on an AMD processor the Hyper-V host must be Windows Server 2022/Windows 11 or greater.
• Advanced: Disable RDP (Remote Desktop Protocol) and WMI (Windows Management Instrumentation) for extreme isolation.
• Advanced: Start Windows kernel debugging with WinDbg.
*1 AIM will perform anti-virus evasion within the virtual machine to ensure that AIM Virtual Machine Tools runs properly.
*2 A file named "AIM_MODIFIED.txt" will be placed on the root of each Windows volume in which AIM Virtual Machine Tools has made adjustments. These adjustments are temporary by design, based on "Write temporary..." mounting.
*3 Depending on your anti-virus software and settings, you may need to exclude/whitelist AIM's folder and/or executables (ArsenalImageMounter.exe and aim_cli.exe) and/or temporarily disable real-time protection to ensure that AIM Virtual Machine Tools will be injected properly. You may also need to instruct your anti-virus software to ignore/allow the "utilman.exe" threat while the VM is booting. In some rare cases you may need to disable your anti-virus software entirely, as we have found one anti-virus product that interferes with AIM even after proper whitelisting.
*4 It is not always possible to identify an authoritative DOMAIN\USER combination for cached domain accounts in certain states, so AIM may create a DOMAIN\USER combination to facilitate access to those accounts. AIM Virtual Machine Tools will display AIM-created DOMAIN\USER combinations in red.
*5 In the case of Active Directory accounts, if a domain controller is available it is quite easy to set up a virtual network between it and the clients (all running in virtual machines launched by AIM), which will allow you to reset account passwords from the domain controller. Resetting passwords in this way will allow you access to previously inaccessible items on the clients such as cached login credentials and EFS-encrypted files and folders.
Additional notes regarding Launch VM:
• Arsenal officially supports launching disk images (or actual physical disks) containing Windows XP Service Pack 1 onward into virtual machines. It is often possible to launch earlier versions of Windows into virtual machines, but with limited functionality from AIM.
• BitLocker "automatic device encryption" can interfere with launching disk images (or actual physical disks) into virtual machines. This problematic BitLocker setting can sometimes be found on new Windows installations as part of the Windows "Out Of Box Experience" or OOBE. Disable this setting (in an OOBE scenario by activating BitLocker protection, turning BitLocker off in "BitLocker Drive Encryption" or in "Device encryption" settings, and if so desired turning BitLocker back on) before launching virtual machines from disk images or actual physical disks.
• The Launch VM button will change to "Reconnect VM" after a VM is launched in order to quickly resolve Hyper-V black screens which sometimes occur on Windows 10 hosts when the view to VMs within Hyper-V consoles is lost, but the VMs are still running fine (which can be confirmed in Hyper-V Manager).
• AIM will create a RAM disk (which is quickly placed offline) when launching certain combinations of operating system, architecture, and partitioning into a VM. The RAM disk created by AIM is temporary, and used to make adjustments to the boot process in situations which include launching Windows 7 (or earlier) on a GPT disk into a VM.
• If you would like to see whether your AIM-launched VMs are Generation 1 or Generation 2, open an administrative PowerShell and run "get-vm | format-list Name,Generation, State"
Mount archive file – Select zip, cab, wim, tar (raw or gzip or bzip2 compressed), or AFF4 (particularly useful with AFF4-L) files to mount read only with CD/DVD-ROM emulation or as a standard file system. If an archive file is mounted as a CD/DVD-ROM, it can be saved as an ISO image or attached directly to a virtual machine, but normal limitations apply such as a 4GB file size limit and a maximum path length of about 60 characters. If an archive file is mounted as a standard file system, there is no limitation on file size or path length, but it cannot be saved as an ISO image or attached directly to a virtual machine. Note that wim files often contain more than one image, so more than one virtual drive may appear. This feature is useful (for example) when you want to perform analysis of particular items within an archive in a read-only manner and/or do not want to extract anything from the archive to the file system.
Mount directory – Select a directory to mount as a read-only virtual CD/DVD-ROM. Includes “Boot image” option to create a bootable CD/DVD by injecting a boot image into the area reserved by the El-Torito standard. This option is sometimes used to boot operating system installation and repair tools (which exist loose in a directory as opposed to on a bootable CD/DVD or in a bootable ISO) into a virtual machine. By default, the "Boot image type" option is set to "NoEmulation" - in other words, it will be set to the typical (and modern) way to configure a boot image. The "Boot image type" option can be set to use floppy or hard disk emulation if desired. This feature is useful (for example) when you want to perform analysis of particular items within a directory in a read-only manner. Also facilitates the attachment of a directory to a virtual machine as if it is a virtual CD/DVD-ROM, which is useful when attaching a directory directly is not possible or you prefer to attach the directory read only.
Create new image file - Create and mount a new disk image file with an NTFS partition. Image formats supported are the same as the "Save as new image" file option. If a raw format is selected, it will be created with the sparse file attribute. The new disk image will be created with 64kb partition alignment, disk and boot code signatures, and fake (but valid) boot code in MBR and VBR. This feature is useful (for example) when testing the behavior of digital forensics tools, file systems, and/or volume and disk encryption.
Save as new image file - Save a “physically” mounted object, including deltas, to a new raw (.raw, .dd, .img, .ima), forensic (.e01), or virtual machine (.vhd, .vhdx, .vdi, .vmdk) disk image. This function can also be used to save virtually mounted objects (such as archives or directories mounted as CD/DVD-ROMs) as raw CD/DVD images, using the extensions .iso and .bin. Some users may leverage this feature to effectively convert from one disk image (or other source) format to another, but please note that (1) the source is saved as it currently appears to Windows - including (for example) injected MBRs and fake disk signatures (which can be applied to the current AIM session even when the disk image is mounted read only - so if this is not what you want, use the AIM CLI to perform conversions from one format to another without mounting being involved) as well as other deltas, and (2) some disk image formats (particularly virtual machine disk image formats) require unique GUIDs within their headers, so for effective hash value comparisons you will need to hash the contents of those disk images rather than the disk images themselves. Also note that when saving to a forensic (.e01) disk image, "default" compression and a 2TB segment size will be used.
Show BitLocker status (all BitLocker-protected volumes) - Displays BitLocker protector IDs and types for all BitLocker-protected volumes within the currently selected disk. Also displays original backup locations for BitLocker recovery keys (e.g. Cloud, File, Printed) and the BitLocker recovery keys themselves (if the volume is currently unlocked).
Unlock BitLocker-protected volumes - Unlocking BitLocker-protected volumes (within the currently selected disk) will decrypt their contents "on the fly" during the current Windows session. The BitLocker-protected data will still be stored encrypted on disk.
Fully decrypt BitLocker-protected volumes - Fully decrypting BitLocker-protected volumes (within the currently selected disk) will completely remove BitLocker, resulting in the previously BitLocker-protected data now being stored unencrypted on disk.
Disable/suspend BitLocker-protected volumes - Disabling (a/k/a suspending) BitLocker-protected volumes (within the currently selected disk) will disable their "protectors" (such as passwords) to allow seamless access to them. The BitLocker-protected data will still be stored encrypted on disk. Disabled BitLocker-protected volumes are sometimes referred to as being in "Clear Key Mode."
Save as fully decrypted image file - Saving BitLocker-protected volumes (within the currently selected disk) to a fully decrypted image file is an efficient way (saving time and disk space) to unlock BitLocker-protected volumes and create a new disk image with any previously BitLocker-protected volumes fully decrypted. Arsenal recommends mounting the BitLockered disk image read-only before using this feature.
Physical disks - AIM's write filter can be applied not only to disk images, but to actual physical disks (fixed and removable) as well. Arsenal strongly recommends using a hardware-based write blocker between a physical disk and a forensic workstation running AIM, but we are aware that some customers use software-based write blockers and other methods (Windows auto-mount policy) to reduce interaction with physical disks. AIM's Advanced/Physical disks dropdown menu options include the following:
• Attach AIM to physical disk...
• Acquire disk image from physical disk...
• Write mounted disk image to physical disk...
• Manage Windows auto-mount policy...
Note: "Write mounted disk image to physical disk..." includes optional free space clearing which will send a TRIM command to TRIM-enabled SSD disks (before writing the mounted disk image) and use traditional clearing (after writing the mounted disk image) for HDDs.
Automatically start Arsenal Image Mounter at logon - Start AIM automatically during logon.
Attach to existing virtual machine - Attach a "physically" mounted object to a virtual machine launched by AIM. If you attach an AIM-mounted disk image, AIM-created RAM disk, or VHDX, you will be asked to place the object offline so that the virtual machine has exclusive access to it. If you attach a folder or archive mounted by AIM with CD/DVD-ROM emulation, you will not need to place the object offline as that concept is not relevant to CD/DVD-ROM emulation. Also, please note that CD/DVD-ROM emulation is inherently read only.
Create RAM disk with fixed memory allocation - Create one or more RAM disks, with fixed memory allocation, as "real" disks containing NTFS file systems. These RAM disks can then be attached to virtual machines, saved to any of AIM's supported disk image formats, and more. RAM disks are sometimes used to meet extreme performance requirements (which HDDs or SSDs cannot) and to temporarily store sensitive files, because they are not written to the physical disk except during hibernation.
Create dynamically allocated RAM disk from VHD template - Create one or more RAM disks, with dynamic memory allocation (memory allocated as files are added to the RAM disk and deallocated when files are deleted), as "real" disks containing NTFS file systems. When using this feature, a VHD image file is selected as a "template" for the RAM disk. The contents of the VHD will be on the RAM disk when it is created, but the VHD will not be modified when the contents of the RAM disk change. These RAM disks can then be attached to virtual machines, saved to any of AIM's supported disk image formats, and more.
Enable virtual dd - Upon enabling the virtual dd function, all available disks, partitions, volumes, and VSCs (whether AIM-mounted/attached or not) will be virtually exposed in a new volume as read-only raw disk images with the “.dd” extension. Disks will be exposed by their “PhysicalDrive” number, partitions will be exposed by their "Harddisk" and then "Partition" numbers, volumes will be exposed both by their currently assigned Windows drive letter and GUID, and VSCs by their volume GUID and timestamp. Please note that (1) CD/DVD discs will be exposed with the ".iso" extension rather than ".dd", (2) keep "Mount VSCs" footnote 1 above in mind when using this feature to work with VSCs, and (3) if AIM is used to mount any of the virtual dd targets, AIM will actually mount the underlying disk, partition, volume, or VSC (with the actual target displayed in parens) in order to reduce extreme technical complexity.
Rescan SCSI bus - Rescanning the SCSI bus may be useful if a disk image mounted by Arsenal Image Mounter does not appear in Windows, or if a dismounted disk image continues to appear there. This is a rarely used feature which is typically used only in debug scenarios involving compatibility issues with AIM and other drivers or applications causing unexpected behavior. The "Rescan Disks" option in Disk Management is similar in effect, except that it will rescan all storage buses instead of the AIM virtual SCSI adapter specifically.
FAQs:
Why is Arsenal Image Mounter different than other disk image mounting solutions?
Many disk image mounting solutions mount the contents of disk images in Windows as shares or partitions (rather than "complete" disks), which limits their usefulness. Arsenal Image Mounter is the first and only open source solution for mounting the contents of disk images as complete disks in Windows. We have also developed a significant amount of functionality that is particularly useful to the digital forensics and incident response community.
What are the requirements for running Arsenal Image Mounter?
Arsenal strongly recommends running Arsenal Image Mounter on Windows 10 (version 1703 or later), 11, or Server 2016/2019 x64 with the latest .NET 6 and on bare metal (i.e. not in a hypervisor) so that all functionality (for example, launching virtual machines and BitLocker-related functionality) works as intended. AIM will run and provide various functionality in many other environments (many users run AIM successfully within various hypervisors), but these other environments are not recommended or supported by Arsenal.
How can I increase performance from disk images mounted by Arsenal Image Mounter?
Storing disk images on the fastest possible storage media is the most efficient way of increasing performance from disk images mounted by Arsenal Image Mounter. Here are benchmarks from launching a Windows 10 disk image (184GB in size, E01 format) into a virtual machine with AIM (all benchmark times are from clicking Launch VM through Windows logon and seeing a user’s Desktop), which demonstrate the drastic differences in performance between disk images stored on hard disk drives (HDDs) and solid-state drives (SSDs):
• Mounted unlocked BitLockered disk image from internal HDD - 4-6 minutes
• Mounted unlocked BitLockered disk image from internal SSD - 2-3 minutes
• Mounted fully decrypted BitLockered disk mage from internal HDD (full decryption took 40-45 minutes) - 3-4 minutes
• Mounted fully decrypted BitLockered disk image from internal SSD (full decryption took 10-15 minutes) - 1 minute
What file systems does Arsenal Image Mounter support?
When mounting disk images using the "Disk Device" mount options, Arsenal Image Mounter essentially "hands off" the contents of disk images to Windows as if they were real SCSI disks, so the file system drivers currently installed on Windows will be used as necessary. Arsenal has used NTFS, FAT32, ReFS, exFAT, HFS+, UFS, and EXT3 file systems contained within AIM-mounted disks successfully when the appropriate file system drivers were installed. AIM also supports bypassing Windows file system drivers and using DiscUtils file system drivers via the "Windows file system driver bypass" mount option.
What disk image formats does Arsenal Image Mounter support?
• Raw (dd)
• Advanced Forensics Format 4 (AFF4) if libaff4 is available
• EnCase (E01 and limited support for Ex01) if libewf is available
• Virtual Machine Disk Files (VHD, VDI, XVA, VMDK, OVA, qcow, qcow2) and checkpoints (AVHD, AVHDX)
What do you mean when you use the phrase "disk images?"
When we use the phrase "disk images" we are using it loosely, in the sense that we are referring to images containing complete disks or partitions, whether they are in raw, virtual machine, or forensic formats.
Why are some files and folders inaccessible to me after mounting a disk image with Arsenal Image Mounter?
Arsenal Image Mounter passes the contents of disk images to Windows as if they were complete disks when using the "Disk Device" mount options. Once AIM has passed the contents of disk images mounted in these modes to Windows, the file system drivers you currently have installed take over and caveats like difficulty accessing protected files and folders may apply.
What file systems does the Windows file system driver bypass mount option support?
• FAT 12/16/32
• NTFS
Experimental support for:
• Btrfs
• Ext2/3/4 (except with 64 bit header fields used by some of the latest Linux distributions)
• ExFAT
• HFS+
• SquashFs
• UDF
• XFS
Can you describe some of the NTFS-related things exposed by the Windows file system driver bypass mount option as well as any NTFS-related limitations?
• NTFS metafiles (for example, $MFT, $LogFile, $UsnJrnl..$J)
• NTFS Alternate Data Streams (ADS) as files suffixed with their stream names alongside the "normal" files they are associated with
• NTFS streams in the [METADATA] folder at the root of each volume. You will find the entire volume's folder structure replicated here, and within each folder you will find the associated streams using the naming convention (STREAMNAME)..(STREAMTYPE). You can also find concatenated stream data for the entire volume at the root of the [METADATA] folder, using the naming convention [(STREAMNAME)]..[(STREAMTYPE)]. The streams currently exposed are $OBJECT_ID, $INDEX_ROOT, $INDEX_ALLOCATION, $EA, and $LOGGED_UTILITY_STREAM.
• Deleted files which have not been completely overwritten will be displayed in the [DELETED] folder at the root of each volume. Filenames will be appended (unless none of their clusters appear to have been reallocated, in which case they will remain as is) to identify what percentage of their clusters have apparently not been reallocated. If you see "[0pct]" appended to a filename, that indicates a very small number of clusters appear to have been reallocated and the percentage has been rounded down to 0. Also, orphans will be displayed within folders using the naming convention MFT-(#)_SEQ-(#). This functionality is based on the DiscUtils project and is best described as "quick file and folder recovery." Please note that while browsing the contents of the [DELETED] folder you may encounter various kinds of corruption related to deleted files and folders (which will result in the error "The disk structure is corrupted and unreadable.") and that the contents of deleted files from SSDs (as opposed to HDDs) will often be empty.
• File slack, unallocated space, and volume slack are exposed at the root of each volume as [SLACK], [UNALLOCATED], and [VOLUME SLACK] respectively. Please note that the volume slack is related to space between the last cluster of the file system and the end of the volume.
• Support for some of the most recent NTFS features (such as CompactOS) are under development and not currently supported.
What is the best mount mode for the purpose of an offline malware scan?
Generally speaking, the best mount mode for an offline malware scan is Windows file system driver bypass as it will provide the malware scanner with access to files that would not be readily accessible otherwise due to file system security. Please note, you may want to exclude certain “files” that AIM exposes in this mount mode which include [SLACK], [UNALLOCATED], [VOLUME SLACK], and possibly the contents of the [DELETED] folder.
Does Arsenal Image Mounter have command-line functionality?
Yes, please see readme_cli.txt for more details. In short, Arsenal Image Mounter CLI (aim_cli.exe) is a .NET 4.0 tool that provides most of Arsenal Image Mounter’s core functionality. The command “AIM_CLI /?” displays basic syntax for using Arsenal Image Mounter CLI. Arsenal Image Mounter CLI is provided with all versions of Arsenal Image Mounter. Arsenal Image Mounter Low Level (aim_ll.exe) is a tool that does not use .NET and provides more “low level” access to the Arsenal Image Mounter driver. The command “AIM_LL /?” displays basic syntax for using Arsenal Image Mounter Low Level. Arsenal Image Mounter Low Level is provided directly by Arsenal.
How can I share files, folders, and/or disks with virtual machines launched by Arsenal Image Mounter?
We normally prefer complete isolation of the virtual machines launched by AIM, but there are plenty of situations in which we need to share files, folders, and/or disks with virtual machines. Some methods of sharing include:
• Enabling guest services in AIM's Launch VM options and then enabling Enhanced Session Mode (by selecting "Connect" at the "Display Configuration” dialog on Windows 8+) while the VM is booting, which will allow copy/paste between the host and virtual machine
• Enabling guest services and Enhanced Session Mode as above will also allow USB drives already attached to the host to be attached to the VM, by selecting “Show Options/Local Resources/Local devices and resources/More…” at the Enhanced Session dialog, then under “Drives” selecting which USB drives to attach to the virtual machine
• Using the Hyper-V Settings/SCSI Controller/Hard Drive/Add/Physical hard disk dropdown to add an offline disk to the VM once it has booted (a "disk" can be a real disk, a VHDX, or a RAM disk created by AIM), which will allow the disk to be used exclusively by the VM
• In Generation 1 VMs, using the Hyper-V Settings/IDE Controller/Physical CD/DVD drive dropdown to add a directory or archive mounted by AIM with CD/DVD-ROM emulation to the VM once it has booted, which will allow the disk to be used simultaneously (but read only!) by the host and VM (if you have more than one directory or archive mounted by AIM and would like to switch between them in the VM, use the aforementioned Physical CD/DVD drive dropdown)
• Using AIM's "Attach to existing virtual machine" feature which effectively replaces the "Hyper-V Settings/SCSI Controller" and "Hyper-V Settings/IDE Controller" methods mentioned above, to create more efficient workflow
• Using PowerShell Direct (if both the host and virtual machine are running Windows 10, 11, or Windows 2016) to run PowerShell commands such as Copy-Item and Enter-PSSession against a virtual machine, regardless of its network or remote management settings. Copy-Item is somewhat self explanatory and Enter-PSSession allows you to run commands within the virtual machine. See https://docs.microsoft.com/en-us/virtualization/hyper-v-on-windows/user-guide/powershell-direct for more information. Please note that PowerShell will ask for credentials of the account within the virtual machine that you are interested in, which you must provide as DOMAIN\USER or COMPUTER\USER. You do not need to enter a password if AIM has already performed Windows authentication bypass... but please note that even though you do not need to provide a password, PowerShell Direct still requires that the account originally had one. Here is sample syntax for Copy-Item first and then Enter-PSSession, each targeting a SANS Windows 10 workstation launched into a VM by AIM:
Copy-Item -FromSession (New-PSSession -VMName AIM_base-rd01-cdrive.e01_5E3437D9) -Path “c:\users\tdungan\documents\demon core.pdf” -Destination c:\users\administrator\desktop
Enter-PSSession -VMName AIM_base-rd01-cdrive.e01_5E3437D9
How can I or my organization contribute to Arsenal Image Mounter?
If Arsenal Image Mounter has become a valuable part of your toolkit, please let your colleagues in digital forensics know. We would also appreciate knowing how you use AIM and if you have any suggestions for future versions. If you or your organization have used AIM source code, APIs, and/or executables in open-source or commercial projects, please make sure you are complying with our licensing requirements. Commercial licensing of AIM source code, APIs, and/or executables helps us offset the cost of continued development, both in terms of Free and Professional Mode functionality.
What does "Create removable disk device" in the "Mount Options" screen do?
This function essentially emulates the attachment of a USB thumb drive. We have heard that it facilitates the mounting of disk images containing partitions rather than disks, even though Arsenal Image Mounter was initially designed to mount disks specifically. Characteristics (and limitations) of using this function include:
• Windows (prior to Windows 10 Build 1703) will only identify and use the first partition in the disk image, even if it contains more than one partition
• SAN policies such as requiring new devices to be mounted offline do not apply
• Drive letters are always assigned even if automatic drive letter assignment is turned off
• Windows identifies and uses file systems even for single-volume disk images that have no partition table
• Inability to interact with Volume Shadow Copies natively
Does Arsenal licensing require an Internet connection?
You only need an Internet connection in regard to Arsenal licensing when you initially activate your license code, once a year for a license validation, and when you extend/renew your license. If you cannot connect to the Internet, please see "How can I activate an Arsenal license on an offline/air-gapped workstation?" below.
How can I activate an Arsenal license on an offline/air-gapped workstation?
If you want your offline/air-gapped workstation properly licensed to run Arsenal Image Mounter and our other tools:
1.) Open Arsenal Image Mounter and enter the license code you were given
2.) Upon realizing that no Internet connection is available, Arsenal Image Mounter will save a “.LIC” file to your ProgramData\ArsenalRecon folder
3.) On a workstation with Internet access, go to our Offline Activation page at https://www.softworkz.com/offline/offline.aspx and upload the “.LIC” file
4.) Finally, copy the CDM file you receive to the ProgramData\ArsenalRecon folder on your offline/air-gapped workstation
Your offline/air-gapped workstation is now ready to run all the Arsenal tools! Please note, if you provide your offline/air-gapped workstation with Internet access for some reason and then launch our tools, the Arsenal license type will be converted from offline to online.
I purchased an Arsenal license extension/renewal, but how do I apply it?
If you have extended/renewed an existing Arsenal license and your forensic workstation has Internet access, but the next time you launch Arsenal Image Mounter you do not see updated subscription information on the Help/About screen..., you can try selecting the "Update license" button. If you have extended/renewed an existing Arsenal license and your forensic workstation is offline, you need to remove the Arsenal license file (.CDM file) from ProgramData\ArsenalRecon and perform another offline activation.
I purchased a new Arsenal license to replace an existing license with an active subscription, but how do I apply it?
If you would like to replace an existing Arsenal license with an active subscription, remove the Arsenal license file (.CDM file) from ProgramData\ArsenalRecon, launch Arsenal Image Mounter, enter the new license code, and follow either the online (if your forensic workstation has Internet access) or offline/air-gapped activation process.
How can I mount and launch virtual machines from disk images containing BitLocker-protected volumes?
When you use Arsenal Image Mounter to mount a disk image containing BitLocker-protected volumes, Windows will recognize those volumes and either ask to unlock them with a key (assuming they were in a locked state) or it will begin real-time decryption without requiring any user input (assuming they were in a disabled or suspended state.) There are a variety of ways in which "BitLockered disk images" (how Arsenal refers to disk images containing one or more BitLocker-protected volumes) can be launched into virtual machines. Here are some examples of workflows to launch BitLockered disk images into virtual machines:
This workflow is what we recommend if you would like maximum performance from the virtual machine:
1.) Use AIM to mount the disk image containing one or more BitLocker-protected volumes in write-temporary mode
2.) Use AIM's "Fully decrypt BitLocker-protected volumes" feature*
3.) Use AIM’s Launch VM feature to launch a virtual machine
4.) Run AIM Virtual Machine Tools by selecting the “Ease of Access” or "Accessibility" icon and use password bypass, etc. as desired
* This feature turns BitLocker off - fully decrypting all the contents of the BitLocker-protected volume. This is a time-consuming process and you can check on the status of full BitLocker decryption by using "manage-bde -status Volume Letter:" at a command prompt. Unlocking (rather than fully decrypting) BitLocker only results in real-time decryption of the BitLocker-protected volume contents as necessary, rather than full decryption.
This workflow is what we recommend for fastest access to the virtual machine (as there is no wait for full decryption):
1.) Use AIM to mount the disk image containing one or more BitLocker-protected volumes in write-temporary mode
2.) Use AIM's "Unlock BitLocker-protected volumes" feature or Windows itself on your forensic workstation to unlock the BitLocker-protected volume(s)
3.) Use AIM’s Launch VM feature to launch a virtual machine and select disable/suspend* BitLocker-protected volumes
4.) Run AIM Virtual Machine Tools by selecting the “Ease of Access” or "Accessibility" icon and use password bypass, etc. as desired
* By disable/suspend, we are referring to exposing the BitLockered volume's encryption key in the clear (the equivalent of "manage-bde -protectors -disable (Volume Letter:)"), turning off any volume protection.
This workflow we do not recommend, because AIM Virtual Machine Tools will not be injected and you will be on your own in terms of logging in to any Windows accounts:
1.) Use AIM to mount the disk image containing one or more BitLockered-protected volumes in write-temporary mode
2.) Do not unlock BitLocker
3.) Use AIM’s Launch VM feature to launch a virtual machine (without allowing AIM to unlock and disable BitLocker protection)
Can I use Arsenal Image Mounter to mount Volume Shadow Copies (VSCs) in Windows natively?
Yes, you can enable Arsenal Image Mounter's “Professional Mode” to access VSC mounting functionality and choose to mount the contents of VSCs three different ways - with the Windows NTFS driver, the DiscUtils NTFS driver, or as a complete disk (with the Windows NTFS driver). You can also leverage AIM’s "Free Mode" disk image mounting functionality along with other tools such as Eric Zimmerman's VSCMount at https://ericzimmerman.github.io/#!index.md or as described on David Cowen’s blog at http://www.hecfblog.com/2014/02/daily-blog-240-arsenal-image-mounter.html.
Can you provide more detail about the Windows bugs identified by Maxim Suhanov and Arsenal which sometimes result in "missing" Volume Shadow Copies?
Maxim Suhanov identified unexpected behavior from the volsnap driver when handling read-only mounted volumes on some insider builds of Windows 10 and a leaked version of Windows 11 - see https://dfir.ru/2021/06/28/shadow-copies-become-less-visible/#more-1212. Due to how Volume Shadow Copy functionality works, Windows actually needs write access to the volume for everything to work properly - even though it has not appeared this way through most builds of Windows 10. The volsnap driver in most Windows 10 builds (other than the recent Insider builds discussed by Maxim Suhanov) would encounter errors when mounting VSCs on volumes mounted read only, but left the symbolic links (such as \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopyXXX) exposed anyway. In the Windows 10 insider builds discussed by Maxim Suhanov and recent Windows 11 builds, the volsnap driver will no longer expose those symbolic links. The workaround for this issue is to mount volumes containing VSCs in write-temporary mode, but Arsenal has identified a different bug in the volsnap driver which sometimes (i.e. non-deterministic behavior) results in "missing" VSCs even when volumes are mounted in write-temporary mode. This bug has been found only in recent Windows 11 builds. When this bug is triggered, VSCs will be deleted by Windows without user notification. Arsenal has reported this bug to Microsoft. The workaround is to re-mount the disk image containing Windows in write-temporary mode. AIM post v3.9.239 contains logic when Windows is mounted in either read only or write-temporary modes which compares a low-level VSC scan against VSCs identified by the volsnap driver (but please be aware this comparison will not be done if the Windows volume is encrypted), and a warning with a recommended solution will be displayed if a delta is found.
How can I release or attach my mouse from a virtual machine launched by AIM?
You can release your mouse from Hyper-V by using the keyboard shortcut CTRL-ALT-LEFT ARROW. In some cases you may find that clicking within the Hyper-V virtual machine does not immediately attach your mouse, but if you wait until the operating system within the virtual machine is ready for input (in other words, it's not busy!) you will then be able to attach your mouse. More keyboard shortcuts can be found at https://blogs.msdn.microsoft.com/virtual_pc_guy/2008/01/14/virtual-machine-connection-key-combinations-with-hyper-v.
Can I use Arsenal Image Mounter to decrypt full-disk or volume encryption within disk images?
Yes, Arsenal Image Mounter is used frequently for this purpose. Generally speaking, you have two great options - use AIM to mount your disk image as a “real” disk and let full-disk or volume encryption software on your host proceed, or launch your disk image into a virtual machine to interact with either the full-disk encryption's limited OS or the volume encryption's native applications within the virtual machine. You can see screenshots from both of these options applied to a disk image containing Symantec Encryption Desktop (a/k/a PGP Desktop) at https://twitter.com/ArsenalRecon/status/1242094213929537540. If you are dealing with BitLocker, AIM also has BitLocker-related functionality to assist you.
Are you having trouble booting decrypted BitLocker volumes?
See Adam Bridge’s excellent blog post on modifying an NTFS volume’s Volume Boot Record (VBR) using Arsenal Image Mounter’s “Write temporary” mode at https://www.contextis.com/resources/blog/making-ntfs-volume-mountable-tinkering-vbr/.
How can I fix AIM’s drop-down menus from flying out beyond the GUI’s borders?
This behavior may be related to Windows Presentation Framework and “handedness.” Your handedness setting can be found by hitting Windows key+R, then pasting in “shell:::{80F3F1D5-FECA-45F3-BC32-752C152E456E}”. If your handedness setting is “Right-handed” you may want to change it to “Left-handed”.
I accidentally set the Hyper-V view so small that I can no longer access the "View" drop-down menu, how can I fix this?
Close Hyper-V, change the "ZoomLevel" value within %UserProfile%\AppData\Roaming\Microsoft\Windows\Hyper-V\Client\1.0\vmconnect.config file to 100 (or at least something larger than it is currently set to.), and then launch another virtual machine.
I have noticed that AIM-mounted (or attached) disks exhibit unusual behavior (e.g. inability to offline disks) on one forensic workstation, but not on others - what could be wrong?
Your forensic workstation may be automatically encrypting (BitLocker protector-free encryption, a/k/a "Clear Key Mode") all newly-attached disks per Windows policy. One method to fix this behavior is to remove BitLocker from your forensic workstation's Windows volume, which will disable this problematic policy. You can then enable BitLocker again without this problematic policy interfering with newly-attached disks.
Will using Hyper-V's "Enhanced Session Mode" cause any problems with Windows virtual machines?
Potentially, yes. We do not recommend using Hyper-V's Enhanced Session Mode (which appears as a "Display Configuration” dialog during the launch of virtual machines running Windows 8+ and essentially uses Remote Desktop to connect to the virtual machine) because unexpected policy issues may surface - for example, accounts may be prohibited from remote and password-less logons. If you are booting a virtual machine and see the Enhanced Session Mode dialog asking about screen resolution, just exit that dialog and you will be returned to direct console mode.
Why isn't Hyper-V running properly on bare metal even though I'm sure it's installed?
If you are sure Hyper-V has been installed, but when you run "sc query HvService" from a command prompt you are notified that it is not running, it's possible that there is an issue with boot configuration due to the presence of other virtualization platforms like VMware or Oracle VM VirtualBox. You may be able to resolve this issue by running "bcdedit /set hypervisorlaunchtype auto" at an administrative command prompt (which will result in Hyper-V starting at boot), but please note that you may need to reverse this action ("bcdedit /set hypervisorlaunchtype off") later to make sure your other virtualization platforms work as expected.
Can I run Hyper-V within VMware or Hyper-V within Hyper-V?
We do not recommend nesting virtualization environments, but some of our customers are doing so successfully. You can find details on running Hyper-V within VMware and Hyper-V within Hyper-V in the Insights article at https://ArsenalRecon.com/insights/arsenal-image-mounter-and-virtual-machine-inception.
Why am I unable to see DPAPI-protected data within a virtual machine running Windows, even though I have an account's actual PIN?
Microsoft accounts (Microsoft (cloud) in AIM Virtual Machine Tools) used in combination with Windows Hello can be configured to require TPM and disable password logons. In the event you launch a virtual machine from a disk image containing a Microsoft account in this condition, you will be able to perform a Windows authentication bypass, but if Arsenal Image Mounter has not already made DPAPI bypass available in the Launch VM options you will not be able to unlock DPAPI to access protected data - even if you have the account’s actual PIN which was used on the original device.
Why am I unable to see DPAPI-protected data within a virtual machine running Windows, even though DPAPI bypass was available to me in the Launch VM Options screen?
Arsenal Image Mounter may be unable to determine prior to Windows booting whether a DPAPI bypass will work successfully when dealing with Microsoft accounts (Microsoft (cloud) in AIM Virtual Machine Tools) that use PIN authentication and have not also used (depending on various circumstances) password or picture-password authentication. If you encounter this scenario, AIM will deliver you to a Windows logon screen rather than the selected user’s Desktop.
Why is it taking so long to mount my disk image?
In some (rare) situations, you may find that it takes an hour or more to mount a disk image. We have found that the conditions are relatively extreme when this occurs - for example, when mounting disk images 10TB+ in size within encrypted volumes on external hard drives and consisting of thousands of segments.
Is it possible to deploy Arsenal Image Mounter unattended?
To some extent, yes. We can provide customers with an installation package containing the Arsenal Image Mounter driver and the AIM CLI application, which can be installed silently depending on circumstances. While the installation will be silent in terms of Arsenal Image Mounter itself, it may not be silent in terms of Windows due to policy - for example, users may need to confirm that they trust drivers from Arsenal.
Is there an Application Programming Interface (API)?
Yes – Arsenal Image Mounter provides both .NET and non-.NET APIs. You can find these APIs on our GitHub page at https://github.com/ArsenalRecon/Arsenal-Image-Mounter/tree/master/API.
What programming languages have been used to build Arsenal Image Mounter?
Arsenal Image Mounter’s Storport miniport driver is written in C and its user mode API library is written in VB.NET, which facilitates easy integration with .NET 4.0 applications.
Where can I find the source code?
Arsenal Image Mounter source code can be found on GitHub at https://github.com/ArsenalRecon/Arsenal-Image-Mounter.
How can I uninstall Arsenal Image Mounter?
If you would like to completely uninstall Arsenal Image Mounter (perhaps you want to revert to an earlier version), go to Device Manager\Storage controllers\Arsenal Image Mounter, right-click and select "Uninstall device". Then, from an administrative command prompt:
1.) [Optional] If you have the Windows Driver Kit (WDK) installed (or Visual Studio, or the Windows SDK), you can run "devcon remove *phdskmnt" (for example, C:\Program Files (x86)\Windows Kits\10\Tools\x64\devcon remove *phdskmnt) instead of using Device Manager
2.) sc delete phdskmnt
3.) sc delete aimwrfltr
4.) [Optional] sc stop vhdaccess
5.) [Optional] sc delete vhdaccess
6.) [Optional] sc stop awealloc
7.) [Optional] sc delete awealloc
8.) [Optional] sc stop dokan1
9.) [Optional] sc delete dokan1
10.) Delete phdskmnt.sys and aimwrfltr.sys from C:\Windows\system32\drivers
11.) [Optional] Delete vhdaccess.sys, awealloc.sys and dokan1.sys from C:\Windows\system32\drivers
12.) Delete the Arsenal Image Mounter executables, libraries, and documentation from where you placed them
Clarifications regarding terminology:
The phrases "Removing disk" and "Unmounting disk image" essentially refer to the same thing when you see them in dialog boxes, documentation, and blog posts related to Arsenal Image Mounter.
Use and License
We chose a dual-license for Arsenal Image Mounter (more specifically, Arsenal Image Mounter’s source code, APIs, and executables) to allow for royalty-free use in open source projects, but require financial support from commercial projects.
Arsenal Consulting, Inc. (d/b/a Arsenal Recon) retains the copyright to Arsenal Image Mounter, including the Arsenal Image Mounter source code, APIs, and executables, being made available under terms of the Affero General Public License v3. Arsenal Image Mounter source code, APIs, and executables may be used in projects that are licensed so as to be compatible with AGPL v3. If your project is not licensed under an AGPL v3 compatible license and you would like to use Arsenal Image Mounter source code, APIs, and/or executables, contact us (sales@ArsenalRecon.com) to obtain alternative licensing.
Contributors to Arsenal Image Mounter must sign the Arsenal Contributor Agreement (“ACA”). The ACA gives Arsenal and the contributor joint copyright interests in the source code.
================================================
FILE: tools/Arsenal-Image-Mounter-v3.10.257/readme_cli.txt
================================================
Please read "Arsenal Recon - End User License Agreement.txt" carefully before using this software.
Arsenal Image Mounter offers two command-line interface executables:
Arsenal Image Mounter CLI (a/k/a AIM CLI, aim_cli.exe) is a .NET tool that provides an integrated command line interface to Arsenal Image Mounter's virtual SCSI miniport driver. Most of Arsenal Image Mounter’s core functionality is available with AIM CLI. The command “AIM_CLI --?” displays basic syntax for using AIM CLI. AIM CLI mounts disk images in read-only mode by default. AIM CLI is provided within the Arsenal Image Mounter download.
Arsenal Image Mounter Low Level (a/k/a AIM LL, aim_ll.exe) is a tool that does not use .NET and provides more “low level” access to the Arsenal Image Mounter driver. The command “AIM_LL --?” displays basic syntax for using AIM LL. AIM LL mounts disk images in write-original mode by default, to maintain compatibility with existing scripts. AIM LL is largely deprecated and is only available directly from Arsenal.
Please note:
• Arsenal Image Mounter CLI and Low Level should normally be run with administrative privileges, but converting disk images from one format to another without mounting (and without using physical disks as targets) and calculating checksums can be done without administrative privileges.
• If you would like to use AIM CLI or LL executables to interact with EnCase (E01 and Ex01), AFF4, or qcow/qcow2 disk images, you must make the Libewf (libewf.dll), LibAFF4 (libaff4.dll), and qcow/qcow2 (libqcow.dll) libraries available in the expected (/lib/x64) or same folder as the AIM CLI or LL executable.
Also note that AIM GUI actively onlines partitions within mounted/attached disks, but AIM CLI does not. Mounting/attaching a disk with AIM CLI is similar to simply attaching a physical disk to Windows without any extra logic, so (for example) Windows policy on the forensic workstation or file system errors within the mounted/attached disk may result (at least initially) in offline partitions and a lack of drive letter assignments.
Particular examples of Arsenal Image Mounter CLI syntax:
#mount an E01 forensic disk image with the write-temporary mount option and fake disk signature, then automatically delete the differencing file
aim_cli.exe --mount --fakesig --filename=C:\path\Win10Disk.E01 --provider=libewf --writeoverlay=C:\path\Win10Disk.E01.diff --autodelete
#mount a dd raw disk image with the write-original mount option
aim_cli.exe --mount --writable --filename=C:\path\Win10Disk.dd
#mount a VMDK virtual machine disk image with the read-only mount option
aim_cli.exe --mount --readonly --filename=C:\path\Win10Disk.vmdk --provider=DiscUtils
#convert an E01 forensic disk image to a new dd raw disk image, without mounting
aim_cli.exe --filename=Win10Disk.E01 --provider=LibEWF --convert=rawconversion.dd
#save an already mounted E01 forensic disk image (using disk id from AIM) to a dd raw disk image
aim_cli.exe --device=000200 --saveas=rawoutput.dd
#save an already mounted E01 forensic disk image (using disk device name from AIM) to a dd raw disk image
aim_cli.exe --device=\\?\physicaldrive4 --saveas=rawoutput.dd
#restore an E01 forensic disk image to an actual physical disk
aim_cli.exe --filename=Win10Disk.E01 --provider=LibEwf --convert=\\?\PhysicalDrive4
Detailed Arsenal Image Mounter CLI syntax:
#mount a raw/forensic/virtual machine disk image as a "real" disk
aim_cli.exe --mount[:removable|:cdrom] [--buffersize=bytes] [--readonly] [--writable] [--fakesig] [--fakembr] --filename=imagefilename --provider=DiscUtils|LibEWF|LibAFF4|MultipartRaw|None [--writeoverlay=differencingimagefile] [--autodelete] [--background]
#start shared memory service mode, for mounting from other applications
aim_cli.exe --name=objectname [--buffersize=bytes] [--readonly] [--writable] [--fakembr] --filename=imagefilename --provider=DiscUtils|LibEWF|LibAFF4|MultipartRaw|None [--background]
#start TCP/IP service mode, for mounting from other computers
aim_cli.exe [--ipaddress=listenaddress] --port=tcpport [--readonly] [--writable] [--fakembr] --filename=imagefilename --provider=DiscUtils|LibEWF|LibAFF4|MultipartRaw|None [--background]
#convert a disk image without mounting
aim_cli.exe [--fakembr] --filename=imagefilename --provider=DiscUtils|LibEWF|LibAFF4|MultipartRaw|None --convert=outputimagefilename [--variant=fixed|dynamic] [--background]
#calculate MD5, SHA1, or SHA256 checksum over disk image contents without mounting (all three caculated if a specific checksum is not specified)
aim_cli.exe --filename=imagefilename --provider=DiscUtils|LibEWF|LibAFF4|MultipartRaw|None --checksum=[MD5|SHA1|SHA256]
#save a new disk image after mounting
aim_cli.exe --device=sixdigitdevicenumber|\\?\physicaldriveN --saveas=outputimagefilename [--variant=fixed|dynamic] [--background]
#dismount a mounted device
aim_cli.exe --dismount[=sixdigitdevicenumber|\\?\physicaldriveN] [--force]
#restore a disk image to an actual physical disk
aim_cli.exe --filename=imagefilename [--fakembr] --provider=DiscUtils|LibEwf|LibAFF4|MultiPartRaw|None --convert=\\?\PhysicalDriveN [--background]
Additional information regarding Arsenal Image Mounter CLI switches:
The --background switch will re-launch AIM CLI in a new process, detach from the current console window, and continue running in the background.
When the --force switch is used in combination with --dismount, the specified device is dismounted even if it may be in use.
The --autodelete switch will automatically delete the differencing file after the disk image is dismounted.
When converting or saving "physical" objects (whether mounted or not), output type for disk images can be raw (.raw, .dd, .img, .ima, .bin), forensic (.e01), virtual machine (.vhd, .vhdx, .vdi, .vmdk), or dmg (.dmg). AIM CLI selects output type based on the outputimagefilename file extension. For virtual machine disk image formats, the optional --variant switch can be used to specify either fixed or dynamically expanding formats - the default is dynamic. This function can also be used to save virtually mounted objects (such as archives or directories mounted as CD/DVD-ROMs) to raw CD/DVD images, using the extensions .iso and .bin.
Use and License
We chose a dual-license for Arsenal Image Mounter (more specifically, Arsenal Image Mounter’s source code, APIs, and executables) to allow for royalty-free use in open source projects, but require financial support from commercial projects.
Arsenal Consulting, Inc. (d/b/a Arsenal Recon) retains the copyright to Arsenal Image Mounter, including the Arsenal Image Mounter source code, APIs, and executables, being made available under terms of the Affero General Public License v3. Arsenal Image Mounter source code, APIs, and executables may be used in projects that are licensed so as to be compatible with AGPL v3. If your project is not licensed under an AGPL v3 compatible license and you would like to use Arsenal Image Mounter source code, APIs, and/or executables, contact us (sales@ArsenalRecon.com) to obtain alternative licensing.
Contributors to Arsenal Image Mounter must sign the Arsenal Contributor Agreement (“ACA”). The ACA gives Arsenal and the contributor joint copyright interests in the source code.
================================================
FILE: tools/sleuthkit-4.12.1-win32/NEWS.txt
================================================
---------------- VERSION 4.12.1 --------------
C/C++:
- Bug fixes from Luis Nassif and Joachim Metz
- Added check to stop for very large folders to prevent memory exhausion
Java:
- Added File Repository concept for files to be stored in another location
- Schema updated to 9.4
- Fixed OS Account merge bug and now fire events when accounts are merged
---------------- VERSION 4.12.0 --------------
- There was a 1-year gap since 4.11.1 and the git log has 441 commits in that timeframe.
- Many for small fixes.
- This set of release notes is much more of an overview than other releases
What's New:
- LVM Support (non-Windows) from Joachim Metz
- Logical File System support (a folder structure is parsed by TSK libraries) from Ann Priestman (Basis)
What's Changed:
- Lots of bug fixes from the Basis team and Joachim Metz
- Additional fixes from Eran-YT, msuhanov, Joel Uckelman, Aleks L, dschoemantruter
- General themes of C/C++ bounds checks and Java improvements to OS Accounts, Ingest jobs, CaseDbAccessManager, and much more.
---------------- VERSION 4.11.1 --------------
C/C++:
- Several fixes from Joachim Metz
- NTFS Decompression bug fix from Kim Stone and Joel Uckelman
Java:
- Fixed connection leak when making OS Accounts in bridge
- OsAccount updates for instance types and special Windows SIDs
- Fixed issue with duplicate value in Japanese timeline translation
---------------- VERSION 4.11.0 --------------
C/C++:
- Added checks at various layers to detect encrypted file systems and disks to give more useful error messages.
- Added checks to detect file formats that are not supported (such as AD1, ZIP, etc.) to give more useful error messages.
- Added tsk_imageinfo tool that detects if an image is supported by TSK and if it is encrypted.
- Add numerous bound checks from Joachim Metz.
- Clarified licenses as pointed out by Joachim Metz.
Java:
- Updated from Schema 8.6 to 9.1.
- Added tables and classes for OS Accounts and Realms (Domains).
- Added tables and classes for Host Addresses (IP, MAC, etc.).
- Added tables and classes for Analysis Results vs Data Artifacts by adding onto BlackboardArtifacts.
- Added tables and classes for Host and Person to make it easier to group data sources.
- Added static types for standard artifact types.
- Added File Attribute table to allow custom information to be stored for each file.
- Made ordering of getting lock and connection consistent.
- Made the findFile methods more efficient by using extension (which is indexed).
---------------- VERSION 4.10.2 --------------
C/C++
- Added support for Ext4 inline data
Java
- New Blackboard Artifacts for ALEAPP/ILEAPP, Yara, Geo Area, etc.
- Upgraded to PostgreSQL JDBC Driver 42.2.18
- Added SHA256 to files table in DB and added utility calculation methods.
- Changed TimelineManager to make events for any artifact with a time stamp
- Added Japanese translations
- Fixed sychronization bug in getUniquePath
---------------- VERSION 4.10.1 --------------
C/C++:
- Changed Windows build to use Nuget for libewf, libvmdk, libvhdi.
- Fixed compiler warnings
- Clarrified licenses and added Apache license to distribution
- Improved error handling for out of memory issues
- Rejistry++ memory leak fixes
Java:
- Localized for Japanese
---------------- VERSION 4.10.0 --------------
C/C++:
- Removed PostgreSQL code (that was used only by Java code)
- Added Java callback support so that database inserts are done in Java.
Java:
- Added methods and callbacks as required to allow database population to happen in Java instead of C/C++.
- Added support to allow Autopsy streaming ingest where files are added in batches.
- Added TaggingManager class and concept of a TagSet to support ProjectVic categories.
- Fixed changes to normalization and validation of emails and phone numbers.
- Added a CASE/UCO JAR file that creates JSON-LD based on TSK objects.
---------------- VERSION 4.9.0 --------------
C/C++
- Removed framework project. Use Autopsy instead if you need an analysis framework.
- Various fixes from Google-based fuzzing.
- Ensure all reads (even big ones) are sector aligned when reading from Windows device.
- Ensure all command line tools support new pool command line arguments.
- Create virtual files for APFS unallocated space
- HFS fix to display type
Java:
- More artifact helper methods
- More artifacts and attributes for drones and GPS coordinates
- Updated TimelineManager to insert GPS artifacts into events table
---------------- VERSION 4.8.0 --------------
C/C++
- Pool layer was added to support APFS. NOTE: API is likely to change.
- Limited APFS support added in libtsk and some of the command line tools.
-- Encryption support is not complete.
-- Blackbag Technologies submitted the initial PR. Basis Technology
did some minor refactoring.
- Refactoring and minor fixes to logical imager
- Various bug fixes from Google fuzzing efforts and Jonathan B from Afarsec
- Fixed infinite NTFS loop from cyclical attribute lists. Reported by X.
- File system bug fixes from uckelman-sf on github
Database:
- DB schema was updated to support pools
- Added concept of JSON in Blackboard Attributes
- Schema supports cascading deletes to enable data source deletion
Java:
- Added Pool class and associated infrastructure
- Added methods to support deleting data sources from database
- Removed JavaFX as a dependency by refactoring the recently
introduced timeline filtering classes.
- Added attachment support to the blackboard helper package.
---------------- VERSION 4.7.0 --------------
C/C++:
- DB schema was expanded to store tsk_events and related tables.
Time-based data is automatically added when files and artifacts are
created. Used by Autopsy timeline.
- Logical Imager can save files as individual files instead of in
VHD (saves space).
- Logical imager produces log of results
- Logical Imager refactor
- Removed PRIuOFF and other macros that caused problems with
signed/unsigned printing. For example, TSK_OFF_T is a signed value
and PRIuOFF would cause problems as it printed a negative number
as a big positive number.
Java
- Travis and Debian package use OpenJDK instead of OracleJDK
- New Blackboard Helper packages (blackboardutils) to make it easier
to make artifacts.
- Blackboard scope was expanded, including the new postArtifact() method
that adds event data to database and broadcasts an event to listeners.
- SleuthkitCase now has an EventBus for database-related events.
- New TimelineManager and associated filter classes to support new events
table
---------------- VERSION 4.6.7 --------------
C/C++ Code:
- First release of new logical imager tool
- VHD image writer fixes for out of space scenarios
Java:
- Expand Communications Manager API
- Performance improvement for SleuthkitCase.addLocalFile()
---------------- VERSION 4.6.6 --------------
C/C++ Code:
- Acquisition deteails are set in DB for E01 files
- Fix NTFS decompression issue (from Joe Sylve)
- Image reading fix when cache fails (Joe Sylve)
- Fix HFS+ issue with large catalog files (Joe Sylve)
- Fix free memory issue in srch_strings (Derrick Karpo)
Java:
- Fix so that local files can be relative
- More Blackboard artifacts and attributes for web data
- Added methods to CaseDbManager to enable checking for and modifying tables.
- APIs to get and set acquisition details
- Added methods to add volume and file systems to database
- Added method to add LayoutFile for allocated files
- Changed handling of JNI handles to better support multiple cases
---------------- VERSION 4.6.5 --------------
C/C++ Code:
- HFS boundary check fix
- New fields for hash values and acquisition details in case database
- Store "created schema version" in case database
Java Code:
- New artifacts and attributes defined
- Fixed bug in SleuthkitCase.getContentById() for data sources
- Fixed bug in LayoutFile.read() that could allow reading past end offile
---------------- VERSION 4.6.4 --------------
Java Code:
- Increase max statements in database to prevent errors under load
- Have a max timeout for SQLite retries
---------------- VERSION 4.6.3 --------------
C/C++ Code:
- Hashdb bug fixes for corrupt indexes and 0 hashes
- New code for testing power of number in ExtX code
Java Code:
- New class that allows generic database access
- New methods that check for duplicate artifacts
- Added caches for frequently used content
Database Schema:
- Added Examiner table
- Tags are now associated with Examiners
- Changed parent_path for logical files to be consistent with FS files.
---------------- VERSION 4.6.2 --------------
C/C++ Code:
- Various compiler warning fixes
- Added small delay into image writer to not starve other threads
Java:
- Added more locking to ensure that handles were not closed while other threads were using them.
- Added APIs to support more queries by data source
- Added memory-based caching when detecting if an object has children or not.
---------------- VERSION 4.6.1 --------------
C/C++ Code:
- Lots of bounds checking fixes from Google's fuzzing tests. Thanks Google.
- Cleanup and fixes from uckelman-sf and others
- PostgreSQL, libvhdi, & libvmdk are supported for Linux / OS X
- Fixed display of NTFS GUID in istat - report from Eric Zimmerman.
- NTFS istat shows details about all FILE_NAME attributes, not just the first. report from Eric Zimmerman.
Java:
- Reports can be URLs
- Reports are Content
- Added APIs for graph view of communications
- JNI library is extracted to name with user name in it to avoid conflicts
Database:
- Version upgraded from to 8.0 because Reports are now Content
---------------- VERSION 4.6.0 --------------
New Features
- New Communications related Java classes and database tables.
- Java build updates for Autopsy Linux build
- Blackboard artifacts are now Content objects in Java and part of tsk_objects table in database.
- Increased cache sizes.
- Lots of bounds checking fixes from Google's fuzzing tests. Thanks Google.
- HFS fix from uckelman-sf.
---------------- VERSION 4.5.0 --------------
New Features:
- Support for LZVN compressed HFS files (from Joel Uckelman)
- Use sector size from E01 (helps with 4k sector sizes)
- More specific version number of DB schema
- New Local Directory type in DB to differentiate with Virtual Directories
- All blackboard artifacts in DB are now 'content'. Attachments can now
be children of their parent message.
- Added extension as a column in tsk_files table.
Bug Fixes:
- Faster resolving of HFS hard links
- Lots of fixes from Google Fuzzing efforts.
---------------- VERSION 4.4.2 --------------
New Features:
- usnjls tool for NTFS USN log (from noxdafox)
- Added index to mime type column in DB
- Use local SQLite3 if it exists (from uckelman-sf)
- Blackboard Artifacts have a shortDescription metho
Bug Fixes:
- Fix for highest HFS+ inum lookup (from uckelman-sf)
- Fix ISO9660 crash
- various performance fixes and added thread safety checks
---------------- VERSION 4.4.1 --------------
- New Features:
-- Can create a sparse VHD file when reading a local drive with new
IMAGE_WRITER structure. Currently being used by Autopsy, but no TSK
command line tools.
- Bug fixes:
-- Lots of cleanup and fixes. Including:
-- memory leaks
-- UTF8 and UTF16 cleanup
-- Missing NTFS files (in fairly rare cases)
-- Really long folder structures and database inserts
---------------- VERSION 4.4.0 --------------
- Compiling in Windows now uses Visual Studio 2015
- tsk_loaddb now adds new files for slack space and JNI was upgraded
accordingly.
---------------- VERSION 4.3.1 --------------
- NTFS works on 4k sectors
- Added support in Java to store local files in encoded form (XORed)
- Added Java Account object into datamodel
- Added notion of a review status to blackboard artifacts
- Upgraded version of PostgreSQL
- Various minor bug fixes
---------------- VERSION 4.3.0 --------------
- PostgreSQL support (Windows only)
- New Release_ NoLibs Visual Studio target
- Support for virtual machine formats via libvmdk and libvhdi (Windows only)
- Schema updates (data sources table, mime type, attributes store type)
- tsk_img_open can take externally created TSK_IMG_INFO
- Various minor bug fixes
---------------- VERSION 4.2.0 --------------
- ExFAT support added
- New database schema
- New Sqlite hash database
- Various bug fixes
- NTFS pays more attention to sequence and loads metadata only
if it matches.
- Added secondary hash database index
---------------- VERSION 4.1.3 --------------
- fixed bug that could crash UFS/ExtX in inode_lookup.
- More bounds checking in ISO9660 code
- Image layer bounds checking
- Update version of SQLITE-JDBC
- changed how java loads navite libraries
- Config file for YAFFS2 spare area
- New method in image layer to return names
- Yaffs2 cleanup.
- Escape all strings in SQLite database
- SQlite code uses NTTFS sequence number to match parent IDs
---------------- VERSION 4.1.2 --------------
Core:
- Fixed more visual studio projects to work on 64-bit
- TskAutoDB considers not finding a VS/FS a critical error.
Java:
- added method to Image to perform sanity check on image sizes.
fiwalk:
- Fixed compile error on Linux etc.
---------------- VERSION 4.1.1 --------------
Core:
- Added FILE_SHARE_WRITE to all windows open calls.
- removed unused methods in CRC code that caused compile errors.
- Added NTFS FNAME times to time2 struct in TSK_FS_META to make them
easier to access -- should have done this a long time ago!
- fls -m and tsk_gettimes output NTFS FNAME times to output for timelines.
- hfind with EnCase hashsets works when DB is specified (and not only index)
- TskAuto now goes into UNALLOC partitions by default too.
- Added support to automatically find all Cellebrite raw dump files given
the name of the first image.
- Added 64-bit windows targets to VisualStudio files.
- Added NTFS sequence to parent address in directory and directory itself.
- Updated SQLite code to use sequence when finding parent object ID.
Java:
- Java bindings JAR files now have native libraries in them.
- Logical files are added with a transaction
---------------- VERSION 4.1.0 --------------
Core:
- Added YAFFS2 support (patch from viaForensics).
- Added Ext4 support (patch from kfairbanks)
- changed all include paths to be 'tsk' instead of 'tsk3'
-- IMPORTANT FOR ALL DEVELOPERS!
Framework:
- Added Linux and MAC support.
- Added L01 support.
- Added APIs to find files by name, path and extension.
- Removed deprecated TskFile::getAttributes methods.
- moved code around for AutoBuild tool support.
Java Bindings:
- added DerivedFile datamodel support
- added a public method to Content to add ability to close() its tsk handle before the object is gc'd
- added faster skip() and random seek support to ReadContentInputStream
- refactored datamodel by pushing common methods up to AbstractFile
- fixed minor memory leaks
- improved regression testing framework for java bindings datamodel
---------------- VERSION 4.0.2 --------------
Core:
New Features:
- Added fiwalk tool from Simson. Not supported in Visual Studio yet.
Bug Fixes:
- Fixed fcat to work on NTFS files (still doesn't support ADS though).
- Fixed HFS+ support in tsk_loaddb / SQLite -- root directory was not added.
- NTFS code now looks at all MFT entries when listing directory contents. It used to only look at unallocated entries for orphan files. This fixes an image that had allocated files missing from the directory b-tree.
- NTFS code uses sequence number when searching MFT entries for all files.
- Libewf detection code change to support v2 API more reliably (ID: 3596212).
- NTFS $SII code could crash in rare cases if $SDS was multiple of block size.
Framework:
- Added new API to TskImgDB that returns the base name of an image.
- Numerous performance improvements to framework.
- Removed requirement in framework to specify module extension in pipeline configuration file.
- Added blackboard artifacts to represent both operating system and network service user accounts.
Java Bindings:
- added more APIs to find files by name, path and where clause
- added API to get currently processed dir when image is being added,
- added API to return specific types of children of image, volume system, volume, file system.
- moved more common methods up to Content interface
- deprecated context of blackboard attributes,
- deprecated SleuthkitCase.runQuery() and SleuthkitCase.closeRunQuery()
- fixed ReadContentInputStream bugs (ignoring offset into a buffer, implementing available() )
- methods that are lazy loading are now thread safe
- Hash class is now thread-safe
- use more PreparedStatements to improve performance
- changed source level from java 1.6 to 1.7
- Throw exceptions from C++ side better
---------------- VERSION 4.0.1 --------------
New Features:
- Can open raw Windows devices with write mode sharing.
- More DOS partition types are displayed.
- Added fcat tool that takes in file name and exports content (equivalent to using ifind and icat together).
- Added new API to TskImgDB that returns hash value associated with carved files.
- performance improvements with FAT code (maps and dir_add)
- performance improvements with NTFS code (maps)
- added AONLY flag to block_walk
- Updated blkls and blkcalc to use AONLY flag -- MUCH faster.
Bug Fixes:
- Fixed mactime issue where it could choose the wrong timezone that did
not follow daylight savings times.
- Fixed file size of alternate data streams in framework.
- Incorporated memory leak fixes and raw device fixes from ADF Solutions.
---------------- VERSION 4.0.0 --------------
New Features:
- Added multithreaded support
- Added C++ wrapper classes
- Added JNI bindings / Java data model classes
- 3314047: Added utf8-specific versions of 'toid' methods for img,vs,fs types
- 3184429: More consistent printing of unset times (all zerso instead of 1970)
- New database design that allows for multiple images in the same database
- GPT volume system tries other sector sizes if first attempt fails.
- Added hash calculation and lookup to AutoDB and JNI.
- Upgraded SQLite to 3.7.9.
- Added Framework in (windows-only)
- EnCase hash support
- Libewf v2 support (it is now non-beta)
- First file in a raw split or E01 can be specified and the rest of the files
are found.
- mactime displays times as 0 if the time is not set (isntead of 1970)
- Changed behavior of 'mactime -y' to use ISO8601 format.
- Updated HFS+ code from ATC-NY.
- FAT orphan file improvements to reduce false positives.
- TskAuto better reports errors.
- Upgrade build projects from Visual Studio 2008 to 2010.
Bug Fixes:
- Relaxed checking when conflict exists between DOS and GPT partitions.
Had a Mac image that was failing to resolve which partition table
to use.
---------------- VERSION 3.2.3 --------------
New Features:
- new TskAuto method (handleNotification()) that gets verbose messages that allow for debugging when the class makes decisions.
- DOS partitions are loaded even if an extended partition fails to load
- new TskAuto::findFilesInFs(TSK_FS_INFO *) method
- Need to only specify first E01 file and the rest are found
- Changed docs license to non-commercial
- Unicode conversion routines fix invalid UTF-16 text during conversion
- Added '-d' to tsk_recover to specify directory to recover
Bug Fixes:
- Added check to fatfs_open to compare first sectors of FAT if we used backup boot sector and verify it is FAT32.
- More checks to make sure that FAT short names are valid ASCII
- 3406523: Mactime size sanity check
- 3393960: hfind reading of Windows input file
- 3316603: Error reading last blocks of RAW CD images
- Fixed bugs in how directories and files were detected in TskAuto
---------------- VERSION 3.2.2 --------------
Bug Fixes
- 3213886: ISO9660 directory hole not advancing
- 3173095 contd: Updated checks so that tougher FAT checks are
applied to deleted directories.
- 3303678: Image type in Sqlite DB is now not always 0
- 3303679: Deleted FAT files have more name cleanup in short names
New Features:
- 3213888: RAW CD format
- Auto class accepts TSK_IMG_INFO as argument
- Copies of split image file names are stored in TSK so that the caller can free them before TSK_IMG_INFO is freed.
---------------- VERSION 3.2.1 --------------
Bug Fixes
- 3108272: fls arguments for -d and -u
- 3105539: compile error issues because of SQlite and pthreads
- 3173095: missing FAT files because of invalid dates.
- 3184419: mingew compile errors.
- 3191391: surround file name in quotes in mactime -d csv output
New Features:
- A single dummy entry is added to the SQlite DB if no volume exists
so that all programs can assume that there will be at least one
volume in the table.
- 3184455: allow srcdir != builddir
---------------- VERSION 3.2.0 --------------
Bug Fixes
- 3043092: Minor logic errors with ifind code.
- FAT performance fix when looking for parent directories
in $OrphanFiles.
- 3052302: Crash on NTFS/UFS detection test because of
corrupt data -- tsk_malloc error.
- 3088447: Error adding attribute because of run collision.
Solved by assigning unique IDs.
New Features:
- 3012324: Name mangling moved out of library into outer tools
so that they can see control characters if they want to. Patch
by Anthony Lawrence.
- 2993806: ENUM values have a specified NONE value if you don't
want to specify any special flags. Patch by Anthony Lawrence.
- 3026989: Add -e and -s flags to img_cat. patch by Simson Garfinkel.
- 2941805: Add case sensitive flag to fsstat in HFS. Patch by Rob Joyce.
- 3017764: Changed how default NTFS $DATA attribute was named. Now it
has no name, while it previously had a fake name of "$Data".
- New TskAuto class.
- New tsk_loaddb, tsk_recover, tsk_comparedir, and tsk_gettimes tools.
---------------- VERSION 3.1.3 --------------
Bug Fixes
- 3006733: FAT directory listings were slow because the inner
code was not stopping when it found the parent directory.
- Adjusted sanity / testing code on FAT directory entries to allow
non-ascii in extensions and reject entries with lots of 0s.
- 3023606: Ext2 / ffs corrupted file names.
- Applied NTFS SID fixes from Mandiant.
- ntfs_load_secure() memory leak patch from Michael Cohen
---------------- VERSION 3.1.2 --------------
Bug Fixes
- 2982426: FAT directory listings were slow because the entire
image was being scanned for parent directory information.
- 2982965: fs_attr length bug fix.
- 2988619: mmls -B display error.
- 2988330: ntfs SII cluster size increment bug
- 2991487: Zeroed content in NTFS files that were not fully initialized.
- 2993767: Slow FAT listings of OrphanFiles because hunt for parent
directory resulted in many searches for OrphanFiles. Added cache
of OrphanFiles.
- 2999567: ifind was not stopping after first hit.
- 2993804: read past end of file did not always return -1.
---------------- VERSION 3.1.1 --------------
Bug Fixes
- 2954703: ISO9660 missing files because duplicate files
had same starting block.
- 2954707: ISO9660 missing some files with zero length and
duplicate starting block. Also changed behavior of how
multiple volume descriptors are processed.
- 2955898: Orphan files not found if no deleted file names exist.
- 2955899: NTFS internal setting of USED flag.
- 2972721: Sorter fails with hash lookup if '-l' is given.
- 2941813: Reverse HFS case sensitive flags (internal fix only)
- 2954448: Debian package typo fixes, etc.
- 2975245: sorter ignores realloc entries to reduce misleading mismatch entries and duplicate entries.
---------------- VERSION 3.1.0 --------------
New Features and Changes
- 2206285: HFS+ can now be read. Lots of tracker items about this.
Thanks to Rob Joyce and ATC-NY for many of the patches and reports.
- 2677069: DOS Safety Partitions in GPT Volume Systems are better
detected instead of reporting multiple VSs.
- Windows executables can be build in Visual Studio w/out needing
other image format libraries.
- 2367426: Uninitialized file space is shown if slack space is
requested.
- 2677107 All image formats supported by AFFLIB can be accessed by
specifying the "afflib" type.
- 2206265: sigfind can now process non-raw files.
- 2206331: Indirect block addresses are now available in the library
and command line tools. They are stored in a different attribute.
- Removed 'docs' files and moved them to the wiki.
- Removed disk_stat and disk_sreset because they were out of date
and hdparm now has the same functionality.
- 2874854: Image layer tools now support non-512 byte device sector
sizes. Users can specify sector size using the -b argument to the
command line tools. This has several consequences:
-- 'mmls -b' is now 'mmls -B'. Similarly with istat -b.
-- Changed command line format for '-o' so that sector size is
specified only via -b and not using '-o 62@4096'.
- 2874852: Sanity checking on partition table entires is relaxed
and only first couple of partitions are checked to make sure that
they can fit into the image.
- 2895607: NTFS SID data is available in the library and 'istat'.
- 2206341: AFF encrypted images now give more proper error message
if password is not given.
- 2351426: mactime is now distributed with Windows execs.
Developer-level Changes
- Abstracted name comparison to file system-specific function.
- Added support in mactime to read body files with comment lines.
- 2596153: Changed img_open arguments, similar to getopt().
- 2797169: tsk_fs_make_ls is now supported as an external library
function. Now named tsk_fs_meta_make_ls.
- 2908510: Nanosecond resolution of timestamps is now available.
- 2914255: Version info is now available in .h files in both string
and integer form.
Bug Fixes:
- 2568528: incorrect adjustment of attribute FILLER offset.
- 2596397: Incorrect date sorting in mactime.
- 2708195: Errors when doing long reads in fragmented attributes.
- Fixed typo bugs in sorter (reported via e-mail by Drew Hunt).
- 2734458: added orphan cache map to prevent slow NTFS listing times.
- 2655831: Sorter now knows about the ext2 and ext3 types.
- 2725799: ifind not converting UTF16 names properly on Windows
because it was using endian ordering of file system and not local
system.
- 2662168: warning messages on macs when reading the raw character
device.
- 2778170: incorrect read size on resident attributes.
- 2777633: missing second resolution on FAT creation times.
- Added the READ_SHARE option to the CreateFile command for split
image files. Patch by Christopher Siwy.
- 2786963: NTFS compression infinite loop fix.
- 2645156: FAT / blkls error getting slack because allocsize was
being set too small (and other values were not being reset).
- 2367426: Zeros are set for VDL slack on NTFS files.
- 2796945: Inifite loop in fs_attr.
- 2821031: Missing fls -m fields.
- 2840345: Extended DOS partitions in extended partitions are now
marked as Meta.
- 2848162: Reading attributes at offsets that are on boundary of
run fragment.
- 2824457: Fixed issue reading last block of file system with blkcat.
- 2891285: Fixed issue that prevented reads from the last block of
a file system when using the POSIX-style API.
- 2825690: Fixed issue that prevented blkls -A from working.
- 2901365: Allow FAT files to have a 0 wdate.
- 2900761: Added FAT directory sanity checks to prevent infinite loops.
- 2895607: Fixed various memory leaks.
- 2907248: Fixed image layer cache crash.
- 2905750: all file system read() functions now return -1 when
offset given is past end of file.
---------------- VERSION 3.0.1 --------------
11/11/08: Bug Fix: Fixed crashing bug in ifind on FAT file system.
Bug: 2265927
11/11/08: Bug Fix: Fixed crashing bug in istat on ExtX $OrphanFiles
dir. Bug: 2266104
11/26/08: Update: Updated fls man page.
11/30/08: Update: Removed TODO file and using tracker for bugs and
feature requests.
12/29/08: Bug Fix: Fixed incorrectly setting block status in file_walk
for compressed files (Bug: 2475246)
12/29/08: Bug Fix: removed fs_info field from FS_META because it
was not being set and should have been removed in 3.0. Reported by
Rob Joyce and Judson Powers.
12/29/08: Bug Fix: orphan files and NTFS files found via parent
directory have an unknown file name type (instead of being equal
to meta type). (Bug: 2389901). Reported by Barry Grundy.
1/12/09: Bug Fix: Fixed ISO9660 bug where large directory contents
were not displayed. (Bug: 2503552). Reported by Tom Black.
1/24/09: Bug Fix: Fixed bug 2534449 where extra NTFS files were
shown if the MFT address was changed to 0 because fs_dir_add was
checking the address and name. Reported by Andy Bontoft.
1/29/09: Update: Fixed fix for bug 2534449. The fix is in ifind
instead of fs_dir_add().
2/2/09: Update: Added RPM spec file from Morgan Weetmam.
---------------- VERSION 3.0.0 --------------
0/00/00: Update: Many, many, many API changes.
2/14/08: Update: Added mmcat tool.
2/26/08: Update: Added flags to mmls to specify partition types.
3/1/08: Update: Major update of man pages.
4/14/08: Bug Fix: Fixed the calculation of "actual" last block.
Off by 1 error. Reported by steve.
5/23/08: Bug Fix: Incorrect malloc return check in srch_strings.
reported by Petri Latvala.
5/29/08: Bug Fix: Fixed endian ordering bug in ISO9660 code. Reported
by Eduardo Aguiar de Oliveira.
6/17/08: Update: 'sorter' now uses the ifind method for finding
deleted NTFS files (like Autopsy) does instead of relying on fls.
Reported by John Lehr.
6/17/08: Update: 'ifind -p' reports data on ADS.
7/10/08: Update: FAT looks for a backup boot sector in FAT32 if
magic is 0
7/21/08: Bug Fix: Changed define of strcasecmp to _stricmp instead
of _strnicmp in Windows. (reported by Darren Bilby).
7/21/08: Bug Fix: Fall back to open "\\.\" image files on Windows
with SHARE_WRITE access so that drive devices can be opened.
(reported by Darren Bilby).
8/20/08: Bug Fix: Look for Windows objects when opening files in
Cygwin, not just Win32. Reported by Par Osterberg Medina.
8/21/08: Update: Renamed library and install header files to have a '3'
in them to allow parallel installations of v2 and v3. Suggested by
Simson Garfinkel.
8/22/08: Update: Added -b option to sorter to specify minimum file size
to process. Suggested by Jeff Kell.
8/22/08: Update: Added libewf as a requirement to build win32 so that
E01 files are supported.
8/29/08: Update: Added initial mingw patches for cross compiling and
Windows. Patches by Michael Cohen.
9/X/08: Update: Added ability to access attibutes
9/6/08: Update: Added image layer cache.
9/12/08: Bug Fix: Fixed crash from incorrectly cleared value in FS_DIR
structure. Reported and patched by Jason Miller.
9/13/08: Update: Changed d* tool names to blk*.
9/17/08: Update: Finished mingw support so that both tools and
library work with Unicode file name support.
9/22/08: Update: Added new HFS+ code from Judson Powers and Rob Joyce (ATC-NY)
9/24/08: Bug Fix: Fixed some cygwin compile errors about types on Cygwin.
Reported by Phil Peacock.
9/25/08: Bug Fix: Added O_BINARY to open() in raw and split because Cygwin
was having problems. Reported by Mark Stam.
10/1/08: Update: Added ifndef to TSK_USE_HFS define to allow people
to define it on the command line. Patch by RB.
---------------- VERSION 2.52 --------------
2/12/08: Bug Fix: Fixed warning messages in mactime about non-Numeric
data. Reported by Pope.
2/19/08: Bug Fix: Added #define to tsk_base_i.h to define
LARGEFILE64_SOURCE based on LARGEFILE_SOURCE for older Linux systems.
2/20/08: Bug Fix: Updated afflib references and code.
3/13/08: Update: Added more fixes to auto* so that AFF will compile
on more systems. I have confirmed that AFFLIB 3.1.3 will run with
OS X 10.4.11.
3/14/08: Bug Fix: Added checks to FAT code that calcs size of
directories. If starting cluster of deleted dir points into a
cluster chain, then problems can occur. Reported by John Ward.
3/19/08: Update: I have verified that this compiles with libewf-20070512.
3/21/08: Bug Fix: Deleted Ext/FFS directories were not being recursed
into. This case was rare (because typically the metadata are
wiped), but possible. Reported by JWalker.
3/24/08: Update: I have verified that this compiles with libewf-20080322.
Updates from Joachim Metz.
3/26/08: Update: Changed some of the header file design for the tools
so that the define settings in tsk_config.h can be used (for large files).
3/28/08: Update: Added config.h reference to srch_strings to get the
LARGEFILE support.
4/5/08: Update: Improved inode argument number parsing function.
---------------- VERSION 2.51 --------------
1/30/08: Bug Fix: Fixed potential infinite loop in fls_lib.c. Patch
by Nathaniel Pierce.
2/7/08: Bug Fix: Defined some of the new constants that are used
in disktools because older Linux distros did not define them.
Reported by Russell Reynolds.
2/7/08: Bug Fix: Modified autoconf to check for large file build
requirements and look for new 48-bit structures needed by disktools.
Both of these were causing problems on older Linux distros.
2/7/08: Update: hfind will normalize hash values in database so
that they are case insensitive.
---------------- VERSION 2.50 --------------
12/19/07: Update: Finished upgrade to autotools building design. No
longer include file, afflib, libewf. Resulted in many source code layout
changes and sorter now searches for md5, sha1, etc.
---------------- VERSION 2.10 --------------
7/12/07: Update: 0s are returned for AFF pages that were not imaged.
7/31/07: Bug Fix: ifind -p could crash if a deleted file name was found
that did not point to a valid meta data stucture. (Reported by Andy Bontoft)
8/5/07: Update: Added NSRL support back into sorter.
8/15/07: Update: Errors are given if supplied sector offset is larger than
disk image. Reported by Simson Garfinkel.
8/16/07: Update: Renamed MD5 and SHA1 functions to TSK_MD5_.. and TSK_SHA_....
8/16/07: Update: tsk_error_get() does not reset the error messages.
9/26/07: Bug Fix: Changed FATFS check for valid dentries to consider
second values of 30. Reported by Alessandro Camillo.
10/18/07: Update: inode_walk for NTFS and FAT will not abort if
data corruption is found in one entry -- instead they will just
skip it.
10/18/07: Update: tsk_os.h uses standard gcc system names instead
of TSK specific ones.
10/18/07: Update: Updated raw.c to use ioctl commands on OS X to
get size of raw device because it does not work with SEEK_END.
Patch by Rob Joyce.
10/31/07: Update: Finished upgrade to fatfs_file_walk_off so that
walking can start at a specific offset. Also finished upgrade that
caches FAT run list to make the fatfs_file_walk_off more efficient.
11/14/07: Update: Fixed few places where off_t was being used
instead of OFF_T. Reported by GiHan Kim.
11/14/07: Update: Fixed a memory leak in aff.c to free AFF_INFO.
Reported by GiHan Kim.
11/24/07: Update: Finished review and update of ISO9660 code.
11/26/07: Bug Fix: Fixed 64-bit calculation in HFS+ code. Submitted
by Rob Joyce.
11/29/07: Update: removed linking of srch_strings.c and libtsk. Reported by
kwizart.
11/30/07: Upate: Made a #define TSK_USE_HFS compile flag for incorporating
the HFS support (flag is in src/fstools/fs_tools_i.h)
11/30/07: Update: restricted the FAT dentry sanity checks to verify
space padding in the name and latin-only extensions.
12/5/07: Bug Fix: fs_read_file_int had a bug that ignored the type passed
for NTFS files. Reported by Dave Collett.
12/12/07: Update: Changed teh FAT dentry sanity checks to allow spaces
in volume labels and do more checking on the attribute flag.
---------------- VERSION 2.09 --------------
4/6/07: Bug Fix: Inifite loop in ext2 and ffs istat code because of using
unsigned size_t variable. Reported by Makoto Shiotsuki.
4/16/07: Bug Fix: Changed use of fseek() to fseeko() in hashtools. Patch
by Andy Bontoft.
4/16/07: Bug Fix: Changed Win32 SetFilePointer to use LARGE_INTEGER.
Reported by Kim GiHan.
4/19/07: Bug Fix: Not all FAT orphan files were being found because of
and offset error.
4/26/07: Bug Fix: ils -O was not working (link value not being
checked). Reported by Christian Perst.
4/27/07: Bug Fix: ils -r was showing UNUSED inodes. Reported by
Christian Perst.
5/10/07: Update: Redefined the USED and UNUSED flags for NTFS so that
UNUSED is set when no attributes exist.
5/16/07: Bug Fix: Fixed several bounds checking bugs that may cause
a crash if the disk image is corrupt. Reported by Tim Newsham (iSec
Partners)
5/17/07: Update: Updated AFFLIB to 2.2.11
5/17/07: Update: Updated libewf to libewf-20070512
5/17/07: Update: Updated file to 4.20
5/29/07: Update: Removed NTFS SID/SDS contributed code because it causes
crashes on some systems and its output is not entirely clear. (most recent bug
reported by Andy Scott)
6/11/07: Update: Updated AFFLIB to 2.2.12.
6/12/07: Bug Fix: ifind -p was not reporting back info on the allocated name
when one existed (because strtok was overwritting the name when the search
continued). Reported by Andy Bontoft.
6/13/07: Update: Updated file to 4.21
---------------- VERSION 2.08 --------------
12/19/06: Bug Fix: ifind_path was not setting *result when root inode
was searched for. patch by David Collett.
12/29/06: Update: Removed 'strncpy' in ntfs.c to manual assignment of
text for '$Data' and 'N/A' for performance reasons.
1/11/07: Update: Added duname to FS_INFO that contains a string of
name for a file system's data unit -- Cluster for example.
1/19/07: Bug Fix: ifind_path was returning an error even after some
files were found. Errors are now ignored if a file was found.
Reported by Michael Cohen.
1/26/07: Bug Fix: Fixed calcuation of inode numbers in fatfs.c
(reported by Simson Garfinkel).
2/1/07: Update: Changed aff-install to support symlinked directory.
2/1/07: Update: img_open modified so that it does not report errors for
s3:// and http:// files that do not exist.
2/5/07: Update: updated *_read() return values to look for "<0" instead of
simply "== -1". (suggested by Simson Garfinkel).
2/8/07: Update: removed typedef for uintptr in WIN32 code.
2/13/07: Update: Applied patch from Kim Kulak to update HFS+ code to internal
design changes.
2/16/07: Update: Renamed many of the external data structures and flags
so that they start with TSK_ or tsk_ to prevent name collisions.
2/16/07: Update: Moved MD5 and SHA1 routines and binaries to auxtools
instead of hashtools so that they are more easy to access.
2/16/07: Update: started redesign and port of hashtools.
2/21/07: Update: Changed inode_walk callback API to remove the flags
variable -- this was redundant since flags are also in TSK_FS_INODE.
Same for TSK_FS_DENT.
3/7/07: Bug Fix: fs_read_file failed for NTFS resident files. Reported
by Michael Cohen.
3/8/07: Bug Fix: FATFS assumed a 512-byte sector in a couple of locations.
3/13/07: Update: Finished hashtools update.
3/13/07: Update: dcat reads block by block instead of all at once.
3/23/07: Update: Change ntfs_load_secure to allocate all of its
needed memory at once instead of doing reallocs.
3/23/07: Update: Updated AFFLIB to 2.2.0
3/24/07: Bug Fix: Fixed many locations where return value from strtoull
was not being properly checked and therefore invalid numbers were not
being detected.
3/24/07: Bug Fix: A couple of error messages in ntfs_file_walk should
have been converted to _RECOVER when the _RECOVERY flag was given.
3/24/07: Update: Changed behavior of ntfs_file_walk. If no type is
given, then a default type is chosen for files and dirs. Now, no error
is generated if that type does not exist -- similar to how no error is
generated if a FAT file has 0 file size.
3/26/07: Update: cleaned up and documented fs_data code more.
3/29/07: Update: Updated AFF to 2.2.2.
3/29/07: Update: Updated install scripts for afflib, libewf, and file to
touch files so that the auto* files are in the correct time stamp order.
4/5/07: Bug Fix: Added sanity checks to offsets and addresses in ExtX and
UFS group descriptors. Reported by Simson Garfinkel.
---------------- VERSION 2.07 --------------
9/6/06: Update: Changed TCHAR and _T to TSK_TCHAR and _TSK_T to avoid
conflicts with other libraries.
9/18/06: Update: Added tsk_list_* functions and structures.
9/18/06: Update: Added checks for recursive FAT directories.
9/20/06: Update: Changed FS_META_* flags for LINK and UNLINK and moved
them to ILS_? flags.
9/20/06: Update: added flags to ils to find only orphan inodes.
9/20/06: Update: Added Orphan support for FAT, NTFS, UFS, Ext2, ISO.
9/20/06: Update: File walk actions now have a flag to identify if a block
is SPARSE or not (used to identify if the address being passed is valid
or made up).
9/21/06: Update: Added file size sanity check to fatfs_is_dentry and
fixed assignment of fatfs->clustcnt.
9/21/06: Update: block_, inode, and dent_walk functions now do more flag
checking and make sure that some things are set instead of making the
calling code do it.
9/21/06: Update: Added checks for recursive (infinite loop) NTFS, UFS,
ExtX, and ISO9660 directories.
9/21/06: Update Added checks to make sure that walking the FAT for files
and directories would result in an infinite loop (if FAT is corrupt).
9/21/06: Update: Added -a and -A to dls to specify allocated and
unallocated blocks to display.
9/21/06: Update: Updated AFFLIB to 1.6.31.
9/22/06: Update: added a fs_read_file() function that allows you to read
random parts of a file.
10/10/06: Update: Improved performance of fs_read_file() and added
new FS_FLAG_META_COMP and FS_FLAG_DATA_COMP flags to show if a file
and data are using file system-level compression (NTFS only).
10/18/06: Bug fix: in fs_data_put_run, added a check to see
if the head was null before looking up. An extra error message
was being created for nothing.
10/18/06: Bug Fix: Added a check to the compression buffer
to see if it is null in _done().
10/25/06: Bug Fix: Added some more bounds checks to NTFS uncompression code.
11/3/06: Bug Fix: added check to dcat_lib in case the number of blocks
requested is too large.
11/07/06: Update: Added fs_read_file_noid wrapper around fs_read_file
interface.
11/09/06: Update: Updated AFF to 1.7.1
11/17/06: Update: Updated libewf to 20061008-1
11/17/06: Bug Fix: Fixed attribute lookup bug in fs_data_lookup.
Patch by David Collett.
11/21/06: Bug Fix: Fixed fs_data loops that were stopping when they hit
an unused attribute. Patch by David Collett.
11/21/06: Bug Fix: sorter no longer clears the path when it starts. THis
was causing errors on Cygwin because OpenSSL libraries could not be found.
11/22/06: Update: Added a tskGetVersion() function to return the string
of the current version.
11/29/06: Update: Added more tsk_error_resets to more places to prevent
extra error messages from being displayed.
11/30/06: Update: Added Caching to the getFAT function and to fs_read.
12/1/06: Update: Changed TSK_LIST to a reverse sorted list of buckets.
12/5/06: Bug Fix: Fixed FS_DATA_INUSE infinite loop bug.
12/5/06: Bug Fix: Fixed infinite loop bug with NTFS decompression code.
12/5/06: Update: Added NULL check to fs_inode_free (from Michael Cohen).
12/5/06: Update: Updated ifind_path so that an allocated name will be
shown if one exists -- do not exit if we find simply an unallocated
entry with an address of 0. Suggested by David Collett.
12/6/06: Update: Updated file to version 4.18.
12/6/06: Update: Updated libaff to 2.0a10 and changed build process
accordingly.
12/7/06: Update: Added a tsk_error_get() function that returns a string
with the error messages -- can be used instead of tsk_error_print.
12/7/06: Update: fixed some memory leaks in FAT and NTFS code.
12/11/06: Bug Fix: fatfs_open error message code referenced a value that
was in freed memory -- reordered statements.
12/15/06: Update: Include VCProj files in build.
---------------- VERSION 2.06 --------------
8/11/06: Bug Fix: Added back in ASCII/UTF-8 checks to remove control
characters in file names.
8/11/06: Bug Fix: Added support for fast sym links in UFS1
8/11/06: Update: Redesigned the endian support so that getuX takes only
the endian flag so that the Unicode design could be changed as well.
8/11/06: Update: Redesigned the Unicode support so that there is a
tsk_UTF... routine instead of fs_UTF...
8/11/06: Update: Updated GPT to fully convert UTF16 to UTF8.
8/11/06: Update: There is now only one aux_tools header file to include
instead of libauxtools and/or aux_lib, which were nearly identical.
8/16/06: Bug Fix: ntfs_dent_walk could segfault if two consecutive
unallocated entries were found that had an MFT entry address of 0.
Reported by Robert-Jan Mora.
8/16/06: Update: Changed a lot of the header files and reduced them so
that it is easier to use the library and only one header file needs to
be included.
8/21/06: Update: mmtools had char * instead of void * for walk callback
8/22/06: Update: Added fs_load_file function that returns a buffer full
with the contents of a file.
8/23/06: Update: Upgraded AFFLIB to 1.6.31 and libewf to 20060820-1.
8/25/06: Update: Created printf wrappers so that output is UTF-16 on
Windows and UTF-8 on Unix.
8/25/06: Update: Continued port to Windows by starting to use more
TCHARS and defining needed macros for the Unix side.
8/25/06: Bug Fix: Fixed crash that could occur because of SDS code
in NTFS. (reported by Simson Garfinkel) (BUG: 1546925).
8/25/06: Bug Fix: Fixed crash that could occur because path stack became
corrupt with deep directories or corrupt images. (reported by Simson
Garfinkel) (BUG: 1546926).
8/25/06: Bug Fix: Fixed infinite loop that could occur when trying to
determine size of FAT directory when the FAT has a loop in it. (BUG:
1546929)
8/25/06: Update: Improved FAT checking code to look for '.' and '..'
entries when inode value is replaced during dent_walk.
8/29/06: Update: Finished Win32 port and changes to handle UTF-16 vs
UTF-8 inputs.
8/29/06: Update: Created a parse_inum function to handle parsing inode
addresses from command line.
8/30/06: Update: Made progname a local variable instead of global.
8/31/06: Bug Fix: Fixed a sizeof() error with the memset in fatfs_inode_walk
for the sect_alloc buffer.
8/31/06: Update: if mktime in dos2unixtime returns any negative value,
then the return value is set to 0. Windows and glibc seem to have
different return values.
---------------- VERSION 2.05 --------------
5/15/06: Bug Fix: Fixed a bug in img_cat that could cause it to
go into an infinite loop. (BUG: 1489284)
5/16/06: Update: Fixed printf statements in tsk_error.c that caused
warning messages for some compilers. Reported by Jason DePriest.
5/17/06: Update: created a union of file system-specific file times in
FS_INFO (Patch by Wyatt Banks)
5/22/06: Bug Fix: Updated libewf to 20060520 to fix bug with reported
image size. (BUG: 1489287)
5/22/06: Bug Fix: Updated AFFLIB to 1.6.24 so that TSK could compile in
CYGWIN. (BUG: 1493013)
5/22/06: Update: Fixed some more printf statements that were causing
compile warnings.
5/23/06: Update: Added a file existence check to img_open to make error
message more accurate.
5/23/06: Update: Usage messages had extra "Supported image types message".
5/25/06: Update: Added block / page range to fsstat for raw and swapfs.
6/5/06: Update: fixed some typos in the output messages of sigfind (reported
by Jelle Smet)
6/9/06: Update: Added HFS+ template to sigfind (Patch by Wyatt Banks)
6/9/06: Update: Added ntfs and HFS template to sigfind.
6/19/06: Update: Begin Windows Visual Studio port
6/22/06: Update: Updated a myflags check in ntfs.c (reported by Wyatt Banks)
6/28/06: Update: Incorporated NTFS compression patch from I.D.E.A.L.
6/28/06: Update: Incorporated NTFS SID patch from I.D.E.A.L.
6/28/06: Bug Fix: A segfault could occur with NTFS if no inode was loaded
in the dent_walk code. (Reported by Pope).
7/5/06: Update: Added tsk_error_reset function and updated code to use it.
7/5/06: Update: Added more sanity checks to the DOS partitions code.
7/10/06: Update: Upgraded libewf to version 20060708.
7/10/06: Update: Upgraded AFFLIB to version 1.6.28
7/10/06: Update: added 'list' option to usage message so that file
system, image, volume system types are listed only if '-x list' is given.
Suggested by kenshin.
7/10/06: Update: Compressed NTFS files use the compression unit size
specified in the header.
7/10/06: Update: Added -R flag to icat to suppress recovery warnings and
use this flag in sorter to prevent FAT recovery messages from filling
up screen.
7/10/06: Update: file_walk functions now return FS_ERR_RECOVERY error
codes for most cases if the RECOVERY flag is set -- this allows the
errors to be more easily suppressed.
7/12/06: Update: Removed individual libraries and now make a single
static libtsk.a library.
7/12/06: Update: Cleaned up top-level Makefile. Use '-C' flag (suggested
by kenshin).
7/14/06: Update: Fixed and redesigned some of the new NTFS compression
code. Changed variable names.
7/20/06: Update: Fixed an NTFS compression bug if a sub-block was not
compressed.
7/21/06: Update: Made NTFS compression code thread friendly.
---------------- VERSION 2.04 --------------
12/1/05: Bug Fix: Fixed a bug in the verbose output of img_open
that would crash if no type or offset was given. Reported and
patched by Wyatt Banks.
12/20/05: Bug Fix: An NTFS directory index sanity check used 356
instead of 365 when calculating an upper bound on the times. Reported
by Wyatt Banks.
12/23/05: Bug Fix: Two printf statements in istat for NTFS printed
to stdout instead of a specific file handle. Reported by Wyatt
Banks.
1/22/06: Bug Fix: fsstat, imgstat and dcalc were using a char instead
of int for the return value of getopt, which caused some systems to not
execute the programs. (internal fix and later reported by Bernhard Reiter)
2/23/06: Update: added support for FreeBSD 6.
2/27/06: Bug Fix: Indirect blocks would nto be found by ifind with
UFS and Ext2. Reported by Nelson G. Mejias-Diaz. (BUG: 1440075)
3/9/06: Update: Added AFF image file support.
3/14/06: Bug Fix: If the first directory entry of a UFS or ExtX block
was unallocated, then later entries may not be shown. Reported by John
Langezaal. (BUG: 1449655)
4/3/06: Update: Finished the improved error handling. Many internal
changes, not many external changes. error() function no longer used
and instead tsk_err variables and function are used. This makes the
library more powerful.
4/5/06: Update: The byte offset for a volume is now passed to the mm_
and fs_ functions instead of img_open. This allows img_info to be used
for multiple volumes at the same time. This required some mm_ changes.
4/5/06: Update: All TSK libraries are written to the lib directory.
4/6/06: Update: Added FS_FLAG_DATA_RES flag to identify data that are
resident in ntfs_data_walk (suggested by Michael Cohen).
4/6/06: Update: The partition code (media Management) now checks that a
partition starts before the end of the image file. There are currently
no checks about the end of the partition though.
4/6/06: Update: The media management code now shows unpartitioned space
as such from the end of the last partition to the end of the image file
(using the image file size). (Suggested by Wyatt Banks).
4/7/06: Update: New version of ISO9660 code from Wyatt Banks and Crucial
Security added and other code updated to allow CDs to be analyzed.
4/7/06: There was a conflict with guessuXX with mmtools and fstools.
Renamed to mm_guessXX and fs_guessXX.
4/10/06: Upgraded AFFLIB to 1.5.6
4/12/06: Added version of libewf and support for it in imgtools
4/13/06: Added new img_cat tool to extract raw data from an image format.
4/24/06: Upgraded AFFLIB to 1.5.12
4/24/06: split and raw check if the image is a directory
4/24/06: Updated libewf to 20060423-1
4/26/06: Updated makedefs to work with SunOS 5.10
5/3/06: Added iso9660 patch from Wyatt Banks so that version number
is not printed with file name.
5/4/06: Updated error checking in icat, istat, fatfs_dent, and ntfs_dent
5/8/06: Updated libewf to 20060505-1 to fix some gcc 2 compile errors.
5/9/06: Updated AFFLIB to 1.6.18
5/11/06: Cleaned up error handling (removed %m and unused legacy code)
5/11/06: Updated AFFLIB to 1.6.23
---------------- VERSION 2.03 --------------
7/26/05: Update: Removed incorrect print_version() statement from
fs_tools.h (reported by Jaime Chang)
7/26/05: Update: Renamed libraries to start with "lib"
7/26/05: Update: Removed the logfp variable for verbose statements
and instead use only stderr.
8/12/05: Update: If time is 0, then it is put as 00:00:00 instead of
the default 1970 or 1980 time.
8/13/05: Update: Added Unicode support for FAT and NTFS (Supported by
I.D.E.A.L. Technology Corp).
9/2/05: Update: Added Unicode support for UFS and ExtX. Non-printable
ASCII characters are no longer replaced with '^.'.
9/2/05: Update: Improved the directory entry sanity checks for UFS
and ExtX.
9/2/05: Update: Upgraded file to version 4.15.
9/2/05: Update: The dent_walk code of all file systems does not
abort if a sub-directory is encountered with an error. If it is the
top directory explicitly called, then it still gives an error.
9/2/05: Bug Fix: MD5 and SHA-1 values were incorrect under AMD64
systems because the incorrect variable sizes were being used.
(reported by: Regis Friend Cassidy. BUG: 1280966)
9/2/05: Update: Changed all licenses in TSK to Common Public License
(except those that were already IBM Public License).
9/15/05: Bug Fix: The Unicode names would not be displayed if the FAT
short name entry was using code pages. The ASCII name check was removed,
which may lead to more false positives during inode_walk.
10/05/05: Update: improved the sector size check when the FAT boot
sector is read (check for specific values besides just mod 512).
10/12/05: Update: The ASCII name check was added back into FAT, but
the check no longer looks for values over 0x80.
10/12/05: Update: The inode_walk function in FAT skips clusters
that are allocated to files. This makes it much faster, but it
will now not find unallocated directory entries in the slack space
of allocated files.
10/13/05: Update: sorter updated to handle unicode in HTML output.
---------------- VERSION 2.02 --------------
4/27/05: Bug Fix: the sizes of 'id' were not consistent in the
front-end and library functions for icat and ffind. Reported by
John Ward.
5/16/05: Bug Fix: fls could segfault in FAT if short name did not
exist. There was also a bug where the long file name variable
(fatfs->lfn_len) was not reset after processing a directory and the
next entry could incorrectly get the long name. Reported by Jaime
Chang. BUG: 1203673.
5/18/05: Update: Updated makedefs to support Darwin 8 (OS X Tiger)
5/23/05: Bug Fix: ntfs_dent_walk would not always stop when WALK_STOP
was returned. This caused some issues with previous versions of ifind.
This was fixed.
5/24/05: Bug Fix: Would not compile under Suse because it had header
file conflicts for the size of int64_t. Reported by: Andrea Ghirardini.
BUG: 1203676
5/25/05: Update: Fixed some memory leaks in fstools (reported by Jaime
Chang).
6/13/05: Update: Compiled with g++ to get better warning messages.
Fixed many signed versus unsigned comparisons, -1 assignments to
unsigned vars, and some other minor internal issues.
6/13/05: Bug Fix: if UFS or FFS found a valid dentry in unallocated
space, it could have a documented length that is larger than the
remaining unallocated space. This would cause an allocated name
to be skipped. BUG: 1210204 Reported by Christopher Betz.
6/13/05: Update: Improved design of all dent code so that there are no
more global variables.
6/13/05: Update: Improved design of FAT dent code so that FATFS_INFO
does not keep track of long file name information.
6/13/05: Bug Fix: If a cluster in a directory started with a strange
dentry, then FAT inode_walk would skip it. The fixis to make sure
that all directory sectors are processed. (BUG: 1203669). Reported
by Jaime Chang.
6/14/05: Update: Changed design of FS_INODE so that it contains the
inode address and the inode_walk action was changed to remove inum
as an argument.
6/15/05: Update: Added 'ils -o' back in as 'ils -O' to list open
and deleted files.
6/15/05: Update: Added '-m' flag to mactime so that it prints the month
as a number instead of its name.
7/2/05: Bug Fix: If an NTFS file did not have a $DATA or $IDX_*
attribute, then fls would not print it. The file had no content, but
the name should be shown. (BUG: 1231515) (Reported by Fuerst)
---------------- VERSION 2.01 --------------
3/24/05: Bug Fix: ffind would fail if the directory had two
non-printable chars. The handling of non-printable chars was changed
to replace with '^.'. (BUG: 1170310) (reported by Brian Baskin)
3/24/05: Bug Fix: icat would not print the output to stdout when split
images were used. There was a bug in the image closing process of
icat. (BUG: 1170309) (reported by Brian Baskin)
3/24/05: Update: Changed the header files in fstools to make fs_lib.h
more self contained.
4/1/05: Bug Fix: Imgtools byte offset with many leading 0s could
cause issues. (BUG: 1174977)
4/1/05: Update: Removed test check in mmtools/dos.c for value cluster
size because to many partition tables have that as a valid field.
Now it checks only OEM name.
4/8/05: Update: Updated usage of 'strtoul' to 'strtoull' for blocks
and inodes.
---------------- VERSION 2.00 --------------
1/6/05: Update: Added '-b' flag to 'mmls' so that sizes can be
printed in bytes. Suggested and a patch proposed by Matt Kucenski
1/6/05: Update: Define DADDR_T, INUM_T, OFF_T, PNUM_T as a static
size and use those to store values in data structures. Updated
print statements as well.
1/6/05: Update: FAT now supports larger images becuase the inode
address space is 64-bits.
1/6/05: Moved guess and get functions to misc from mmtools and
fstools.
1/7/05: Update: Added imgtools with support for "raw" and "split"
layers. All fstools have been updated.
1/7/05: Update: removed dtime from ils output
1/9/05: Update: FAT code reads in clusters instead of sectors to
be faster (suggested by David Collett)
1/9/05: Update: mmtools uses imgtools for split images etc.
1/10/05: Update: Removed usage of global variables when using
file_walk internally.
1/10/05: Update: mmls BSD will use the next sector automatically
if the wrong is given instead of giving an error.
1/10/05: Update: Updated file to version 4.12
1/11/05: Update: Added autodetect to file system tools.
1/11/05: Update: Changed names to specify file system type (not
OS-based)
1/11/05: Update: Added '-t' option to fsstat to give just the type.
1/11/05: Update: Added autodetect to mmls
1/17/05: Update: Added the 'mmstat' tool that gives the type of
volume system.
1/17/05: Update: Now using CVS for local version control - added
date stamps to all files.
2/20/05: Bug Fix: ils / istat would go into an infinte loop if the
attribute list had an entry with a length of 0. Reported by Angus
Marshall (BUG: 1144846)
3/2/05: Update: non-printable letters in ExtX/UFS file names are
now replaced by a '.'
3/2/05: Update: Made file system tools more library friendly by
making stubs for each application.
3/4/05: Update: Redesigned the diskstat tool and created the
disksreset tool to remove the HPA temporarily.
3/4/05: Update: Added imgstat tool that displays image format
details
3/7/05: Bug Fix: In fsstat on ExtX, the final group would have an
incorrect _percentage_ of free blocks value (although the actual
number was correct). Reported by Knut Eckstein. (BUG: 1158620)
3/11/05: Update: Renamed diskstat, disksreset, sstrings, and imgstat to
disk_stat, disk_sreset, srch_strings, and img_stat to make the names more
clear.
3/13/05: Bug Fix: The verbose output for fatfs_file_walk had an
incorrect sector address. Reported by Rudolph Pereira.
3/13/05: Bug Fix: The beta version had compiling problems on FreeBSD
because of a naming clash with the new 'fls' functions. (reported
by secman)
---------------- VERSION 1.74 --------------
11/18/04: Bug Fix: FreeBSD 5 would produce incorrect 'icat' output for
Ext2/3 & UFS1 images because it used a 64-bit on-disk address.
reported by neutrino neutrino. (BUG: 1068771)
11/30/04: Bug Fix: The makefile in disktools would generate an error
on some systems (Cygwin) because of an extra entry. Reported by
Vajira Ganepola (BUG: 1076029)
---------------- VERSION 1.73 --------------
09/09/04: Update: Added journal support for EXT3FS and added jls
and jcat tools.
09/13/04: Updated: Added the major and minor device numbers to
EXTxFS istat.
09/13/04: Update: Added EXTxFS orphan code to 'fsstat'
09/24/04: Update: Fixed incorrect usage of 'ptr' and "" in action
of ntfs_dent.c. Did not affect any code, but could have in the
future. Reported by Pete Winkler.
09/25/04: Update: Added UFS flags to fsstat
09/26/04: Update: All fragments are printed for indirect block pointer
addresses in UFS istat.
09/29/04: Update: Print extended UFS2 attributes in 'istat'
10/07/04: Bug Fix: Changed usage of (int) to (uintptr_t) for pointer
arithmetic. Caused issues with Debian Sarge. (BUG: 1049352) - turned out
to be from changes made to package version so that it would compile in
64-bit system (BUG: 928278).
10/11/04: Update: Added diskstat to check for HPA on linux systems.
10/13/04: Update: Added root directory location to FAT32 fsstat output
10/17/04: Bug Fix: EXTxFS superblock location would not be printed
for images in fsstat that did not have sparse superblok (which is
rare) (BUG: 1049355)
10/17/04: Update: Added sigfind tool to find binary signatures.
10/27/04: Bug Fix: NTFS is_clust_alloc returned an error when loading
$MFT that had attribute list entry. Now I assume that clusters
referred to by the $MFT are allocated until the $MFT is loaded.
(BUG: 1055862).
10/28/04: Bug Fix: Check to see if an attribute with the same name
exists instead of relying on id only. (ntfs_proc_attrseq) Affects
the processing of attribute lists. Reported by Szakacsits Szabolcs,
Matt Kucenski, & Gene Meltser (BUG: 1055862)
10/28/04: Update: Removed usage of mylseek in fstools for all systems
(Bug: 928278)
---------------- VERSION 1.72 --------------
07/31/04: Update: Added flag to mft_lookup so that ifind can run in noabort
mode and it will not stop when it finds an invalid magic value.
08/01/04: Update: Removed previous change and removed MAGIC check
entirely. XP doesn't even care if the Magic is corrupt, so neither
does TSK. The update sequence check should find an invalid MFT
entry.
08/01/04: Update: Added error message to 'ifind' if none of the search
options are given.
08/05/04: Bug Fix: Fixed g_curdirptr recursive error by clearing the value
when dent_walk had to abort because a deleted directory could not be recovered.
(BUG: 1004329) Reported by epsilon@yahoo.com
08/16/04: Update: Added a sanity check to fatfs.c fat2unixtime to check
if the year is > 137 (which is the overflow date for the 32-bit UNIX time).
08/16/04: Update: Added first version of sstrings from binutils-2.15
08/20/04: Bug Fix: Fixed a bug where the group number for block 0 of an
EXT2FS file system would report -1. 'dstat' no longer displays value when it
is not part of a block group. (BUG: 1013227)
8/24/04: Update: If an attribute list entry is found with an invalid MFT
entry address, then it is ignored instead of an error being generated and
exiting.
8/26/04: Update: Changed internal design of NTFS to make is_clust_alloc
8/26/04: Update: If an attribute list entry is found with an invalid MFT
entry address AND the entry is unallocated, then no error message is
printed, it is just ignored or logged in verbose mode.
8/29/04: Update: Added support for 32-bit GID and UID in EXTxFS
8/30/04: Bug Fix: ntfs_dent_walk was adding 24 extra bytes to the
size of the index record for the final record processing (calc of
list_len) (BUG: 1019321) (reported and debugging help from Matt
Kucenski).
8/30/04: Bug Fix: fs_data_lookup was using an id of 0 as a wild
card, but 0 is a legit id value and this could cause confusion. To
solve this, a new FS_FLAG_FILE_NOID flag was added and a new
fs_data_lookup_noid function that will not use the id to lookup
values. (BUG: 1019690) (reported and debugging help from Matt
Kucenski)
8/30/04: Update: modified fs_data_lookup_noid to return unamed data
attribute if that type is requested (instead of just relying on id
value in attributes)
8/31/04: Update: Updated file to v4.10, which seems to fix the
CYGWIN compile problem.
9/1/04: Update: Added more DOS partition types to mmls (submitted by
Matt Kucenski)
9/2/04: Update: Added EXT3FS extended attributes and Posix ACL to istat
output.
9/2/04: Update: Added free inode and block counts per group to fsstat for
EXT2FS.
9/7/04: Bug Fix: FreeBSD compile error for PRIx printf stuff in mmtools/gpt.c
---------------- VERSION 1.71 --------------
06/05/04: Update: Added sanity checks in fat to unix time conversion so that
invalid times are set to 0.
06/08/04: Bug Fix: Added a type cast when size is assigned in FAT
and removed the assignment to a 32-bit signed variable (which was no
longer needed). (Bug: 966839)
06/09/04: Bug Fix: Added a type cast to the 'getuX' macros because some
compilers were assuming it was signed (Bug: 966839).
06/11/04: Update: Changed NTFS magic check to use the aa55 at the
end and fixed the name of the original "magic" value to oemname.
The oemname is now printed in fsstat.
06/12/04: Bug Fix: The NTFS serial number was being printed with
bytes in the wrong order in the fsstat output. (BUG: 972207)
06/12/04: Update: The begin offset value in index header for NTFS
was 16-bits instead of 32-bits.
06/22/04: Update: Created a library for the MD5 and SHA1 functions so
that it can be incorporated into other tools. Also renamed some of the
indexing tools that hfind uses.
06/23/04: Update: Changed output of 'istat' for NTFS images. Added more
data from $STANDARD_INFORMATION.
07/13/04: Update: Changed output of 'istat' for NTFS images again. Moved
more data to the $FILE_NAME section and added new data.
07/13/04: Update: Changed code for processing NTFS runs and no
longer check for the offset to be 0 in ntfs_make_data_run(). This
could have prevented some sparse files from being processed.
07/13/04: Update: Added flags for compressed and encrypted NTFS
files. They are not decrypted or uncompressed yet, just identified.
They cannot be displayed from 'icat', but the known layout is given
in 'istat'.
07/18/04: Bug Fix: Sometimes, 'icat' would report an error about an
existing FILLER entry in an NTFS attribute. This was traced to
instances when it was run on a non-base file record. There is now
a check for that to not show the error. (BUG: 993459)
07/19/04: Bug Fix: A run of -1 may exist for sparse files in non-NT
versions of NTFS. Changed check for this. reported by Matthew
Kucenski. (BUG: 994024).
07/24/04: Bug Fix: NTFS attribute names were missing (rarely) on
some files because the code assumed they would always be at offset
64 for non-res attributes (Bug: 996981).
07/24/04: Update: Made listing of unallcoated NTFS file names less
strict. There was a check for file name length versus stream length.
07/24/04: Update: Added $OBJECT_ID output to 'istat'
07/24/04: Update: Fixed ntfs.c compile warning about constant too
large in time conversion code.
07/25/04: Update: Added attribute list contents to NTFS 'istat' output
07/25/04: Bug Fix: Not all slack space was being shown with 'dls -s'.
It was documented that this occurs, but it is not what would be
expected. (BUG: 997800).
07/25/04: Update: Changed output format of 'dls -s' so that it sends
zeros where the file content was. Therefore the output is now a
multiple of the data unit size. Also removed limitation to FAT &
NTFS.
07/25/04: Update: 'dcalc' now has the '-s' option calculate the
original location of data from a slack space image (dls -s).
(from Chris Betz).
07/26/04: Update: Created the fs_os.h file and adjusted some of the
header files for the PRI macros (C99). Created defines for OSes that do
not have the macros already defined.
07/26/04: Non-release bug fix: Fixed file record size bug introduced with
recent changes.
07/27/04: Update: Added GPT support to mmls.
07/29/04: Update: Added '-p' flag to 'ifind' to find deleted NTFS files
that point to the given parent directory. Added '-l and -z' as well.
---------------- VERSION 1.70 --------------
04/21/04: Update: Changed attribute and mode for FAT 'istat' so
that actual FAT attributes are used instead of UNIX translation.
04/21/04: Update: The FAT 'istat' output better handles Long FIle
Name entry
04/21/04: Update: The FAT 'istat' output better handles Volume Label
entry
04/21/04: Update: Allowed the FAT volume label entry to be displayed
with 'ils'
04/21/04: Update: Allowed the FAT volume label entry to be displayed
with 'fls'
04/24/04: Update: 'dstat' on a FAT cluster now shows the cluster
address in addition to the sector address.
04/24/04: Update: Added the cluster range to the FAT 'fsstat' output
05/01/04: Update: Improved the FAT version autodetect code.
05/02/04: Update: Removed 'H' flag from 'icat'.
05/02/04: Update: Changed all of the FS_FLAG_XXX variables in the
file system tools to constants that are specific to the usage
(NAME, DATA, META, FILE).
05/03/04: Update: fatfs_inode_walk now goes by sectors instead of clusters
to get more dentries from slack space.
05/03/04: Bug Fix: The allocation status of FAT dentires was set only by
the flag and not the allocation status of the cluster it is located in.
(BUG: 947112)
05/03/04: Update: Improved comments and variable names in FAT code
05/03/04: Update: Added '-r' flag to 'icat' for deleted file recovery
05/03/04: Update: Added RECOVERY flag to file_walk for deleted file
recovery
05/03/04: Update: Added FAT file recovery.
05/03/04: Update: Removed '-H' flag from 'icat'. Default is to
display holes.
05/03/04: Update: 'fls -r' will recurse down deleted directories in FAT
05/03/04: Update: 'fsstat' reports FAT clusters that are marked as BAD
05/03/04: Update: 'istat' for FAT now shows recovery clusters for
deleted files.
05/04/04: Update: Added output to 'fsstat' for FAT file systems by adding
a list of BAD sectors and improving the amount of layout information. I
also changed some of the internal variables.
05/08/04: Update: Removed addr_bsize from FS_INFO, moved block_frags
to FFS_INFO, modified dcat output only data unit size.
05/20/04: Update: Added RECOVERY flag to 'ifind' so that it can find the
data units that are allocated to deleted files
05/20/04: Update: Added icat recovery options to 'sorter'.
05/20/04: Update: Improved the naming convention in sorter for the 'ils'
dead files.
05/21/04: Update: Added outlook to sorter rules (from David Berger)
05/27/04: Bug Fix: Added to mylseek.c so that it compiles
with Fedora Core 2 (Patch by Angus Marshall) (BUG: 961908).
05/27/04: Update: Changed the letter with 'fls -l' for FIFO to 'p'
instead of 'f' (reported by Dave Henkewick).
05/28/04: Update: Added '-u' flag to 'dcat' so that the data unit size
can be specified for raw, swap, and dls image types.
05/28/04: Update: Changed the size argument of 'dcat' to be number of
data units instead of size in bytes (suggestion by Harald Katzer).
---------------- VERSION 1.69 --------------
03/06/04: Update: Fixed some memory leaks in ext2fs_close. reported
by Paul Bakker.
03/10/04: Bug Fix: If the '-s' flag was used with 'icat' on a EXT2FS
or FFS file system, then a large amount of extra data came out.
Reported by epsion. (BUG: 913874)
03/10/04: Bug Fix: One of the verbose outputs in ext2fs.c was being sent
to STDOUT instead of logfp. (BUG: 913875)
04/14/04: Update: Added more data to fsstat output of FAT file system.
04/15/04: Bug Fix: The last sector of a FAT file system may not
be analyzed. (BUG: 935976)
04/16/04: Update: Added full support for swap and raw by making the
standard files and functions for them instead of the hack in dcat.
Suggested by (and initial patch by) Paul Baker.
04/18/04: Update: Changed error messages in EXT2/3FS code to be extXfs.
04/18/04: Update: Updaged to version 4.09 of 'file'. This will
help fix some of the problems people have had compiling it under
OS X 10.3.
04/18/04: Update: Added compiling support for SFU 3.5 (Microsoft). Patches
from an anonymous person.
---------------- VERSION 1.68 --------------
01/20/04: Bug Fix: FAT times were an hour too fast during daylight savings.
Now use mktime() instead of manual calculation. Reported by Randall
Shane. (BUG: 880606)
02/01/04: Update: 'hfind -i' now reports the header entry as an invalid
entry. The first header row was ignored.
02/20/04: Bug Fix: indirect block pointer blocks would not be identified by
the ifind tool. Reported by Knut Eckstein (BUG: 902709)
03/01/04: Update: Added fs->seek_pos check to fs_read_random.
---------------- VERSION 1.67 --------------
11/15/03: Bug Fix: Added support for OS X 10.3 to src/makedefs. (BUG: 843029)
11/16/03: Bug Fix: Mac partition tables could generate an error if there were
VOID-type partitions. (BUG: 843366)
11/21/03: Update: Changed NOABORT messages to verbose messages, so invalid
data is not printed during 'ifind' searches.
11/30/03: Bug Fix: icat would not hide the 'holes' if '-h' was given because
the _UNALLOC flag was always being passed to file_walk. (reported by
Knut Eckstein). (BUG: 851873)
11/30/03: Bug Fix: NTFS data_walk was not using _ALLOC and _UNALLOC flags
and other code that called it was not either. (BUG: 851895)
11/30/03: Bug Fix: Not all needed commands were using _UNALLOC when they
called file_walk (although for most cases it did not matter because
sparse files would not be found in a directory for example). (Bug: 851897)
12/09/03: Bug Fix: FFS and EXT2FS code was using OFF_T type instead of
size_t for the size of the file. This could result in a file > 2GB
as being a negative size on some systems (BUG: 856957).
12/26/03: Bug Fix: ffind would crash for root directory of FAT image.
Added NULL check and added a NULL name to fake root directory entry.
(BUG: 871219)
01/05/04: Bug Fix: The clustcnt value for FAT was incorrectly calculated
and was too large for FAT12 and FAT16 by 32 sectors. This could produce
extra entries in the 'fsstat' output when the FAT is dumped.
(BUG: 871220)
01/05/04: Bug Fix: ils, fls, and istat were not printing the full size
of files that are > 2GB. (reported by Knut Eckstein) (BUG: 871457)
01/05/04: Bug Fix: The EXT2FS and EXT3FS code was not using the
i_dir_acl value as the upper 32-bits of regular files that are
> 2GB (BUG: 871458)
01/06/04: Mitigation: An error was reported where sorter would error
that icat was being passed a '-1' argument. I can't find how that would
happen, so I added quotes to all arguments so that the next time it
occurs, the error is more useful (BUG: 845840).
01/06/04: Update: Incorporated patch from Charles Seeger so that 'cc'
can be used and compile time warnings are fixed with Sun 'cc'.
01/06/04: Update: Upgraded file from v3.41 to v4.07
---------------- VERSION 1.66 --------------
09/02/03: Bug Fix: Would not compile under OpenBSD 3 because fs_tools.h
& mm_tools was missing a defined statement (reported by Randy - m0th_man)
NOTE: Bugs now will have an entry into the Source Forge bug tracking
sytem.
10/13/03: Bug Fix: buffer was not being cleared between uses and length
incorrectly set in NTFS resulted in false deleted file names being shown
when the '-r' flag was given. The extra entries were from the previous
directory. (BUG: 823057)
10/13/03: Bug Fix: The results of 'sorter' varied depending on the version
of Perl and the system. If the file output matched more than one,
sorter could not gaurantee which would match. Therefore, results were
different for some files and some machines. 'sorter' now enforces the
ordering based on the order they are in the configuration file. The
entries at the end of the file have priority over the first entries
(generic rules to specific rules). (BUG: 823057)
10/14/03: Update: 'mmls' prints 'MS LVM' with partition type 0x42 now.
10/25/03: Bug Fix: NTFS could have a null pointer crash if the image
was very corrupt and $Data was not found for the MFT.
11/10/03: Bug Fix: NTFS 'ffind' would only report the file name and not
the attribute name because the type and id were ignored. ffind and
ntfs_dent were updated - found during NTFS keyword search test.
(Bug: 831579()
11/12/03: Update: added support for Solaris x86 partition tables to 'mmls'
11/12/03: Update: Modified the sparc data structure to add the correct
location of the 'sanity' magic value.
11/15/03: Update: Added '-s' flag to 'icat' so that slack space is also
displayed.
---------------- VERSION 1.65 --------------
08/03/03: Bug Fix: 'sorter' now checks for inode values that are too
small to avoid 'icat' errors about invalid inode values.
08/19/03: Update: 'raw' is now a valid type for 'dcat'.
08/21/03: Update: mactime and sorter look for perl5.6.0 first.
08/21/03: Update: Removed NSRL support from 'sorter' until a better
wany to identify the known good and known bad files is found
08/21/03: Bug Fix: The file path replaces < and > with HTML
encoding for HTML output (ils names were not being shown)
08/25/03: Update: Added 'nsrl.txt' describing why the NSRL functionality
was removed.
08/27/03: Update: Improved code in 'mactime' to reduce warnings when
'-w' is used with Perl ('exists' checks on arrays).
08/27/03: Update: Improved code in 'sorter' to reduce warnings when
'-w' is used with Perl (inode_int for NTFS).
---------------- VERSION 1.64 --------------
08/01/03: Docs Fix: The Sun VTOC was documented as Virtual TOC and it
should be Volume TOC (Jake @ UMASS).
08/02/03: Bug Fix: Some compilers complained about verbose logging
assignment in 'mmls' (Ralf Spenneberg).
---------------- VERSION 1.63 --------------
06/13/03; Update: Added 'mmtools' directory with 'dos' partitions
and 'mmls'.
06/18/03: Update: Updated the documents in the 'doc' directory
06/19/03: Update: Updated error message for EXT3FS magic check
06/27/03: Update: Added slot & table number to mmls
07/08/03: Update: Added mac support to mmtools
07/11/03: Bug Fix: 'sorter' was not processing all unallocated meta
data structures because of a regexp error. (reported by Jeff Reava)
07/16/03: Update: Added support for FreeBSD5
07/16/03: Update: Added BSD disk labels to mmtools
07/28/03: Update: Relaxed requirements for DOS directory entries, the wtime
can be zero (reported by Adam Uccello).
07/30/03: Update: Added SUN VTOC to mmtools
07/31/03: Update: Added NetBSD support (adam@monkeybyte.org)
08/01/03: Update: Added more sanity checks to FAT so that it would not
try and process NTFS images that have the same MAGIC value
---------------- VERSION 1.62 --------------
04/11/03: Bug Fix: 'fsstat' for an FFS file system could report data
fragments in the last group that were larger than the maximum
fragment
04/11/03: Bug Fix: 'ffs' allows the image to not be a multiple of the
block size. A read error occurred when it tried to read the last
fragments since a whole block could not be read.
04/15/03: Update: Added debug statements to FAT code.
04/26/03: Update: Added verbose statements to FAT code
04/26/03: Update: Added NOABORT flag to dls -s
04/26/03: Update: Added stderr messages for errors that are not aborted
because of NOABORT
05/27/03: Update: Added 'mask' field to FATFS_INFO structure and changed
code in fatfs.c to use it.
05/27/03: Update: isdentry now checks the starting cluster to see if
it is a valid size.
05/27/03: Bug Fix: Added a sanitizer to 'sorter' to remove invalid chars
from the 'file' output and reduce the warnings from Perl.
05/28/03: Bug Fix: Improved sanitize expression in 'sorter'
05/28/03: Update: Added '-d' option to 'mactime' to allow output to be
given in comma delimited format for importing into a spread sheet or
other graphing tool
06/09/03: Update: Added hourly summary / indexing to mactime
06/09/03: Bug Fix: sorter would not allow linux-ext3 fstype
---------------- VERSION 1.61 --------------
02/05/03: Update: Started addition of image thumbnails to sorter
03/05/03: Update: Updated 'file' to version 3.41
03/16/03: Update: Added comments and NULL check to 'ifind'
03/16/03: Bug Fix: Added a valid magic of 0 for MFT entries. This was
found in an XP image.
03/26/03: Bug Fix: fls would crash for an inode of 0 and a clock skew
was given. fixed the bug in fls.c (debug help from Josep Homs)
03/26/03: Update: Added more verbose comments to ntfs_dent.c.
03/26/03: Bug Fix: 'ifind' for a path could return a result that was
shorter than the requested name (strncmp was used)
03/26/03: Update: Short FAT names can be used in 'ifind -n' and
error messages were improved
03/26/03: Bug Fix: A final NTFS Index Buffer was not always processed in
ntfs_dent.c, which resulted in files not being shown. This was fixed
with debugging help from Matthew Shannon.
03/27/03: Update: Added an 'index.html' for image thumbnails in sorter
and added a 'details' link from the thumbnail to the images.html file
03/27/03: Update: 'sorter' can now take a directory inode to start
processing
03/27/03: Update: added '-z' flag when running 'file' in 'sorter' so that
compressed file contents are reported
03/27/03: Update: added '-i' flag to 'mactime' that creates a daily
summary of events
03/27/03: Update: Added support for Version 2 of the NSRL in 'hfind'
04/01/03: Update: Added support for Hash Keeper to 'hfind'
04/01/03: Update: Added '-e' flag to 'hfind' for extended info
(currently hashkeeper only)
---------------- VERSION 1.60 --------------
10/31/02: Bug Fix: the unmounting status of EXT2FS in the 'fsstat' command
was not correct (reported by Stephane Denis).
11/24/02: Bug Fix: The -v argument was not allowed on istat or fls (Michael
Stone)
11/24/02: Bug Fix: When doing an 'ifind' on a UNIX fs, it could abort if it
looked at an unallocated inode with invalid indirect block pointers.
This was fixed by adding a "NOABORT" flag to the walk code and adding
error checks in the file system code instead of relying on the fs_io
code. (suggested by Micael Stone)
11/26/02: Update: ifind has a '-n' argument that allows one to specify a
file name it and it searches to find the meta data structure for it
(suggested by William Salusky).
11/26/02: Update: Now that there is a '-n' flag with 'ifind', the '-d'
flag was added to specify the data unit address. The old syntax of
giving the data_unit at the end is no longer supported.
11/27/02: Update: Added sanity checks on meta data and data unit addresses
earlier in the code.
12/12/02: Update: Added additional debug statements to NTFS code
12/19/02: Update: Moved 'hash' directory to 'hashtools'
12/19/02: Update: Started development of 'hfind'
12/31/02: Update: Improved verbose debug statements to show full 64-bit
offsets
01/02/03: Update: Finished development of 'hfind' with ability to update
for next version of NSRL (which may have a different format)
01/05/03: Bug Fix: FFS and EXT2FS symbolic link destinations where not
properly NULL terminated and some extra chars were appended in 'fls'
(later reported by Thorsten Zachmann)
01/06/03: Bug Fix: getu64() was not properly masking byte sizes and some
data was being lost. This caused incorrect times to be displayed in some
NTFS files.
01/06/03: Bug Fix: ifind reported incorrect ownership for some UNIX
file systems if the end fragments were allocated to a different file than
the first ones were.
01/07/03: Update: Renamed the src/mactime directory to src/timeline.
01/07/03: Update: Updated README and man pages for hfind and sorter
01/12/03: Bug Fix: ntfs_mft_lookup was casting a 64-bit value to a 32-bit
variable. This caused MFT Magic errors. Reported and debugged by
Keven Murphy
01/12/03: Update: Added verbose argument to 'fls'
01/12/03: Bug Fix: '-V' argument to 'istat' was doing verbose instead of
version
01/13/03: Update: Changed static sizes of OFF_T and DADDR_T in Linux
version to the actual 'off_t' and 'daddr_t' types
01/23/03: Update: Changed use of strtok_r to strtok in ifind.c so that
Mac 10.1 could compile (Dave Goldsmith).
01/28/03: Update: Improved code in 'hfind' and 'sorter' to handle
files with spaces in the path (Dave Goldsmith).
---------------- VERSION 1.52 --------------
09/24/02: Bug Fix: Memory leak in ntfs_dent_idxentry(), ntfs_find_file(),
and ntfs_dent_walk()
09/24/02: Update: Removal of index sequences for index buffers is now
done using upd_off, which will allow for NTFS to move the structure in
the future.
09/26/02: Update: Added create time for NTFS / STANDARD_INFO to
istat output.
09/26/02: Update: Changed the method that the NTFS time is converted
to UNIX time. Should be more efficient.
10/09/02: Update: dcat error changed.
10/02/02: Update: Includes a Beta version of 'sorter'
---------------- VERSION 1.51 --------------
09/10/02: Bug Fix: Fixed a design bug that would not allow attribute
lists in $MFT. This bug would generate an error that complained about
an invalid MFT entry in attribute list.
09/10/02: Update: The size of files and directories is now calculated
after each time proc_attrseq() is called so that it is more up to date
when dealing with attribute lists. The size has the sizes of all
$Data, $IDX_ROOT, and $IDX_ALLOC streams.
09/10/02: Update: The maxinum number of MFT entries is now calculated
each time an MFT entry is processed while loading the MFT. This
allows us to reflect what the maximum possible MFT entry is at that
given point based on how many attribute lists have been processed.
09/10/02: Update: Added file version 3.39 to distro (bigger magic files)
(Salusky)
09/10/02: Bug Fix: fs_data was wasting memory when it was allocated
09/10/02: Update: added a fs_data_alloc() function
09/12/02: Bug Fix: Do not give an error if an attribute list of an
unallocated file points to an MFT that no longer claims it is a
member of the list.
09/12/02: Update: No longer need version to remove update sequence
values from on-disk buffers
09/19/02: Bug Fix: fixed memory leak in ntfs_load_ver()
09/19/02: Bug Fix: Update sequence errors were displayed because of a
bug that occurred when an MFT entry crossed a run in $MFT. Only occurred
with 512-byte clusters and an odd number of clusters in a run.
09/19/02: Update: New argument to ils, istat, and fls that allows user to
specify a time skew in seconds of the compromised system. Originated
from discussion at DFRWS II.
09/19/02: Update: Added '-h' argument to mactime to display header info
---------------- VERSION 1.50 --------------
04/21/02: icat now displays idxroot attribute for NTFS directories
04/21/02: fs_dent_print functions now are passed the FS_DATA structure
instead of the extra inode and name strings. (NTFS)
04/21/02: fs_dent_print functions display alternate data stream size instead
of the default data size (NTFS)
04/24/02: Fixed bug in istat that displayed too many fragments with ffs images
04/24/02: Fixed bug in istat that did not display sparse files correctly
04/24/02: fsstat of FFS images now identifies the fragments at the
beginning of cyl groups as data fragments.
04/26/02: Fixed bug in ext2fs_dent_parse_block that did not advance the
directory entry pointer far enough each time
04/26/02: Fixed bug in ext2fs_dent_parse_block so that gave an error if
a file name was exactly 255 chars
04/29/02: Removed the getX functions from get.c as they are now macros
05/11/02: Added support for lowercase flag in FAT
05/11/02: Added support for sequence values (NTFS)
05/13/02: Added FS_FLAG_META for FAT
05/13/02: Changed ifind so that it looks the block up to identify if it is
a meta data block when an inode can not be found
05/13/02: Added a conditional to ifind so that it handles sparse files better
05/19/02: Changed icat so that the default attribute type is set in the
file_walk function
05/20/02: ils and dls now use boundary inode & block values if too large
or small are given
05/21/02: istat now displays all NTFS times
05/21/02: Created functions to just display date and time
05/24/02: moved istat functionality to the specific file system file
05/25/02: added linux-ext3 flag, but no new features
05/25/02: Added sha1 (so Autopsy can use the NIST SW Database)
05/26/02: Fixed bug with FAT that did not return all slack space on file_walk
05/26/02: Added '-s' flag to dls to extract slack space of FAT and NTFS
06/07/02: fixed _timezone variable so correct times are shown in CYGWIN
06/11/02: *_copy_inode now sets the flags for the inode
06/11/02: fixed bug in mactimes that displayed a duplicate entry with time
because of header entries in body file
06/12/02: Added ntfs.README doc
06/16/02: Added a comment to file Makefile to make it easier to compile for
an IR CD.
06/18/02: Fixed NTFS bug that showed ADS when only deleted files were supposed
to be shown (when ADS in directory)
06/19/02: added the day of the week to the mactime output (Tan)
07/09/02: Fixed bug that added extra chars to end of symlink destination
07/17/02: 1.50 Released
---------------- VERSION 1.00 --------------
- Integrated TCT-1.09 and TCTUTILs-1.01
- Fixed bug in bcat if size is not given with type of swap.
- Added platform indep by including the structures of each file system type
- Added flags for large file support under linux
- blockcalc was off by 1 if calculated using the raw block number and
not the one that lazarus spits out (which start at 1)
- Changed the inode_walk and block_walk functions slightly to return a
value so that a walk can be ended in the middle of it.
- FAT support added
- Improved ifind to better handle fragments
- '-z' flag to fls and istat now use the time zone string instead of
integer value.
- no longer prepend / in _dent
- verify that '-m' directory in fls ends with a '/'
- identify the destination of sym links
- fsstat tool added
- fixed caching bug with FAT12 when the value overlapped cache entries
- added mactime
- removed the value in fls when printing mac format (inode is now printed in mactime)
- renamed src/misc directory to src/hash (it only has md5 and will have sha)
- renamed aux directory to misc (Windows doesn't allow aux as a name ??)
- Added support for Cygwin
- Use the flags in super block of EXT2FS to identify v1 or v2
- removed file system types of linux1 and linux2 and linux
- added file system type of linux-ext2 (as ext3 is becoming more popular)
- bug in file command that reported seek error for object files and STDIN
================================================
FILE: tools/sleuthkit-4.12.1-win32/README-win32.txt
================================================
The Sleuth Kit
Windows Executables
http://www.sleuthkit.org/sleuthkit
Brian Carrier [carrier@sleuthkit.org]
Last Updated: July 2012
======================================================================
This zip file contains the Microsoft Windows executables for The Sleuth
Kit. The full source code (including Visual Studio Solution files) and
documentation can be downloaded from:
http://www.sleuthkit.org
These are distributed under the IBM Public License and the Common
Public License, which can be found in the licenses folder.
NOTES
The dll files in the zip file are required to run the executables. They
must be either in the same directory as the executables or in the path.
There have been reports of the exe files not running on some systems
and they give the error "The system cannot execute the specified program".
This occurs because the system can't find the needed dll files. Installing
the "Microsoft Visual C++ 2008 SP1 Redistributable Package (x86)" seems
to fix the problem. It can be downloaded from Microsoft:
http://www.microsoft.com/downloads/en/confirmation.aspx?FamilyID=A5C84275-3B97-4AB7-A40D-3802B2AF5FC2&displaylang=en
mactime.pl requires a Windows port of Perl to be installed. If you have
the ".pl" extension associated with Perl, you should be able to run
"mactime.pl" from the command line. Otherwise, you may need to run it
as "perl mactime.pl". Examples of Windows ports of Perl include:
- ActivePerl (http://www.activestate.com/activeperl/)
- Strawberry Perl (http://strawberryperl.com/)
CURRENT LIMITATIONS
The tools do not currently support globbing, which means that you
cannot use 'fls img.*' on a split image. Windows does not automatically
expand the '*' to all file names. However, most split images can now
be used in The Sleuth Kit by simply specifying the first segment's path.
These programs can be run on a live system, if you use the
\\.\PhysicalDrive0 syntax. Note though, that you may get errors or the
file system type may not be detected because the data being read is out
of sync with cached versions of the data.
Unicode characters are not always properly displayed in the command
shell.
The AFF image formats are not supported.
================================================
FILE: tools/sleuthkit-4.12.1-win32/README.txt
================================================
[](https://travis-ci.org/sleuthkit/sleuthkit)
[](https://ci.appveyor.com/project/bcarrier/sleuthkit)
# [The Sleuth Kit](http://www.sleuthkit.org/sleuthkit)
## INTRODUCTION
The Sleuth Kit is an open source forensic toolkit for analyzing
Microsoft and UNIX file systems and disks. The Sleuth Kit enables
investigators to identify and recover evidence from images acquired
during incident response or from live systems. The Sleuth Kit is
open source, which allows investigators to verify the actions of
the tool or customize it to specific needs.
The Sleuth Kit uses code from the file system analysis tools of
The Coroner's Toolkit (TCT) by Wietse Venema and Dan Farmer. The
TCT code was modified for platform independence. In addition,
support was added for the NTFS (see [wiki/ntfs](http://wiki.sleuthkit.org/index.php?title=NTFS_Implementation_Notes))
and FAT (see [wiki/fat](http://wiki.sleuthkit.org/index.php?title=FAT_Implementation_Notes)) file systems. Previously, The Sleuth Kit was
called The @stake Sleuth Kit (TASK). The Sleuth Kit is now independent
of any commercial or academic organizations.
It is recommended that these command line tools can be used with
the Autopsy Forensic Browser. Autopsy, (http://www.sleuthkit.org/autopsy),
is a graphical interface to the tools of The Sleuth Kit and automates
many of the procedures and provides features such as image searching
and MD5 image integrity checks.
As with any investigation tool, any results found with The Sleuth
Kit should be be recreated with a second tool to verify the data.
## OVERVIEW
The Sleuth Kit allows one to analyze a disk or file system image
created by 'dd', or a similar application that creates a raw image.
These tools are low-level and each performs a single task. When
used together, they can perform a full analysis. For a more detailed
description of these tools, refer to [wiki/filesystem](http://wiki.sleuthkit.org/index.php?title=TSK_Tool_Overview).
The tools are briefly described in a file system layered approach. Each
tool name begins with a letter that is assigned to the layer.
### File System Layer:
A disk contains one or more partitions (or slices). Each of these
partitions contain a file system. Examples of file systems include
the Berkeley Fast File System (FFS), Extended 2 File System (EXT2FS),
File Allocation Table (FAT), and New Technologies File System (NTFS).
The fsstat tool displays file system details in an ASCII format.
Examples of data in this display include volume name, last mounting
time, and the details about each "group" in UNIX file systems.
### Content Layer (block):
The content layer of a file system contains the actual file content,
or data. Data is stored in large chunks, with names such as blocks,
fragments, and clusters. All tools in this layer begin with the letters
'blk'.
The blkcat tool can be used to display the contents of a specific unit of
the file system (similar to what 'dd' can do with a few arguments).
The unit size is file system dependent. The 'blkls' tool displays the
contents of all unallocated units of a file system, resulting in a
stream of bytes of deleted content. The output can be searched for
deleted file content. The 'blkcalc' program allows one to identify the
unit location in the original image of a unit in the 'blkls' generated
image.
A new feature of The Sleuth Kit from TCT is the '-l' argument to
'blkls' (or 'unrm' in TCT). This argument lists the details for data
units, similar to the 'ils' command. The 'blkstat' tool displays
the statistics of a specific data unit (including allocation status
and group number).
### Metadata Layer (inode):
The metadata layer describes a file or directory. This layer contains
descriptive data such as dates and size as well as the addresses of the
data units. This layer describes the file in terms that the computer
can process efficiently. The structures that the data is stored in
have names such as inode and directory entry. All tools in this layer
begin with an 'i'.
The 'ils' program lists some values of the metadata structures.
By default, it will only list the unallocated ones. The 'istat'
displays metadata information in an ASCII format about a specific
structure. New to The Sleuth Kit is that 'istat' will display the
destination of symbolic links. The 'icat' function displays the
contents of the data units allocated to the metadata structure
(similar to the UNIX cat(1) command). The 'ifind' tool will identify
which metadata structure has allocated a given content unit or
file name.
Refer to the [ntfs wiki](http://wiki.sleuthkit.org/index.php?title=NTFS_Implementation_Notes)
for information on addressing metadata attributes in NTFS.
### Human Interface Layer (file):
The human interface layer allows one to interact with files in a
manner that is more convenient than directly with the metadata
layer. In some operating systems there are separate structures for
the metadata and human interface layers while others combine them.
All tools in this layer begin with the letter 'f'.
The 'fls' program lists file and directory names. This tool will
display the names of deleted files as well. The 'ffind' program will
identify the name of the file that has allocated a given metadata
structure. With some file systems, deleted files will be identified.
#### Time Line Generation
Time lines are useful to quickly get a picture of file activity.
Using The Sleuth Kit a time line of file MAC times can be easily
made. The mactime (TCT) program takes as input the 'body' file
that was generated by fls and ils. To get data on allocated and
unallocated file names, use 'fls -rm dir' and for unallocated inodes
use 'ils -m'. Note that the behavior of these tools are different
than in TCT. For more information, refer to [wiki/mactime](http://wiki.sleuthkit.org/index.php?title=Mactime).
#### Hash Databases
Hash databases are used to quickly identify if a file is known. The
MD5 or SHA-1 hash of a file is taken and a database is used to identify
if it has been seen before. This allows identification to occur even
if a file has been renamed.
The Sleuth Kit includes the 'md5' and 'sha1' tools to generate
hashes of files and other data.
Also included is the 'hfind' tool. The 'hfind' tool allows one to create
an index of a hash database and perform quick lookups using a binary
search algorithm. The 'hfind' tool can perform lookups on the NIST
National Software Reference Library (NSRL) (www.nsrl.nist.gov) and
files created from the 'md5' or 'md5sum' command. Refer to the
[wiki/hfind](http://wiki.sleuthkit.org/index.php?title=Hfind) file for more details.
#### File Type Categories
Different types of files typically have different internal structure.
The 'file' command comes with most versions of UNIX and a copy is
also distributed with The Sleuth Kit. This is used to identify
the type of file or other data regardless of its name and extension.
It can even be used on a given data unit to help identify what file
used that unit for storage. Note that the 'file' command typically
uses data in the first bytes of a file so it may not be able to
identify a file type based on the middle blocks or clusters.
The 'sorter' program in The Sleuth Kit will use other Sleuth Kit
tools to sort the files in a file system image into categories.
The categories are based on rule sets in configuration files. The
'sorter' tool will also use hash databases to flag known bad files
and ignore known good files. Refer to the [wiki/sorter](http://wiki.sleuthkit.org/index.php?title=Sorter)
file for more details.
## LICENSE
There are a variety of licenses used in TSK based on where they
were first developed. The licenses are located in the [licenses
directory](https://github.com/sleuthkit/sleuthkit/tree/develop/licenses).
- The file system tools (in the
[tools/fstools](https://github.com/sleuthkit/sleuthkit/tree/develop/tools/fstools)
directory) are released under the IBM open source license and Common
Public License.
- srch_strings and fiwalk are released under the GNU Public License
- Other tools in the tools directory are Common Public License
- The modifications to 'mactime' from the original 'mactime' in TCT
and 'mac-daddy' are released under the Common Public License.
The library uses utilities that were released under MIT and BSD 3-clause.
## INSTALL
For installation instructions, refer to the INSTALL.txt document.
## OTHER DOCS
The [wiki](http://wiki.sleuthkit.org/index.php?title=Main_Page) contains documents that
describe the provided tools in more detail. The Sleuth Kit Informer is a newsletter that contains
new documentation and articles.
> www.sleuthkit.org/informer/
## MAILING LIST
Mailing lists exist on SourceForge, for both users and a low-volume
announcements list.
> http://sourceforge.net/mail/?group_id=55685
Brian Carrier
carrier at sleuthkit dot org
================================================
FILE: tools/sleuthkit-4.12.1-win32/bin/mactime.pl
================================================
my $VER="4.12.1";
#
# This program is based on the 'mactime' program by Dan Farmer and
# and the 'mac_daddy' program by Rob Lee.
#
# It takes as input data from either 'ils -m' or 'fls -m' (from The Sleuth
# Kit) or 'mac-robber'.
# Based on the dates as arguments given, the data is sorted by and
# printed.
#
# The Sleuth Kit
# Brian Carrier [carrier sleuthkit [dot] org]
# Copyright (c) 2003-2012 Brian Carrier. All rights reserved
#
# TASK
# Copyright (c) 2002 Brian Carrier, @stake Inc. All rights reserved
#
#
# The modifications to the original mactime are distributed under
# the Common Public License 1.0
#
#
# Copyright 1999 by Dan Farmer. All rights reserved. Some individual
# files may be covered by other copyrights (this will be noted in the
# file itself.)
#
# Redistribution and use in source and binary forms are permitted
# provided that this entire copyright notice is duplicated in all such
# copies.
#
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR ANY PARTICULAR PURPOSE.
#
# IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, LOSS OF USE, DATA, OR PROFITS OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
use POSIX;
use strict;
my $debug = 0;
# %month_to_digit = ("Jan", 1, "Feb", 2, "Mar", 3, "Apr", 4, "May", 5, "Jun", 6,
# "Jul", 7, "Aug", 8, "Sep", 9, "Oct", 10, "Nov", 11, "Dec", 12);
my %digit_to_month = (
"01", "Jan", "02", "Feb", "03", "Mar", "04", "Apr",
"05", "May", "06", "Jun", "07", "Jul", "08", "Aug",
"09", "Sep", "10", "Oct", "11", "Nov", "12", "Dec"
);
my %digit_to_day = (
"0", "Sun", "1", "Mon", "2", "Tue", "3", "Wed",
"4", "Thu", "5", "Fri", "6", "Sat"
);
sub usage {
print <all_names() ) {
push( @t_list, $_ );
}
foreach( keys( %{DateTime::TimeZone->links()}) ) {
push( @t_list, $_ );
}
return sort { $a cmp $b } @t_list;
}
usage() if (scalar(@ARGV) == 0);
while ((scalar(@ARGV) > 0) && (($_ = $ARGV[0]) =~ /^-(.)(.*)/)) {
# Body File
if (/^-b$/) {
shift(@ARGV);
if (defined $ARGV[0]) {
$BODY = $ARGV[0];
}
else {
print "-b requires body file argument\n";
}
}
elsif (/^-d$/) {
$COMMA = 1;
}
# Group File
elsif (/^-g$/) {
shift(@ARGV);
if (defined $ARGV[0]) {
&'load_group_info($ARGV[0]);
$GROUP = $ARGV[0];
}
else {
print "-g requires group file argument\n";
usage();
}
}
# Password File
elsif (/^-p$/) {
shift(@ARGV);
if (defined $ARGV[0]) {
&'load_passwd_info($ARGV[0]);
$PASSWD = $ARGV[0];
}
else {
print "-p requires password file argument\n";
usage();
}
}
elsif (/^-h$/) {
$header = 1;
}
# Index File
elsif (/^-i$/) {
shift(@ARGV);
if (defined $ARGV[0]) {
if ($INDEX ne "") {
print "Only one -i argument can be supplied\n";
usage();
}
# Find out what type
if ($ARGV[0] eq "day") {
$INDEX_TYPE = $INDEX_DAY;
}
elsif ($ARGV[0] eq "hour") {
$INDEX_TYPE = $INDEX_HOUR;
}
shift(@ARGV);
unless (defined $ARGV[0]) {
print "-i requires index file argument\n";
usage();
}
$INDEX = $ARGV[0];
}
else {
print "-i requires index file argument and type\n";
usage();
}
open(INDEX, ">$INDEX") or die "Can not open $INDEX";
}
elsif (/^-V$/) {
version();
exit(0);
}
elsif (/^-m$/) {
$month_num = 1;
}
elsif (/^-y$/) {
$iso8601 = 1;
}
elsif (/^-z$/) {
shift(@ARGV);
if (defined $ARGV[0]) {
my $tz = "$ARGV[0]";
if ($tz =~ m/^list$/i) {
if ($_HAS_DATETIME_TIMEZONE) {
my $txt = "
-----------------------------------
TIMEZONE LIST
-----------------------------------\n";
foreach ( get_timezone_list() ) {
$txt .= $_ . "\n";
}
print( $txt );
}
else {
print "DateTime module not loaded -- cannot list timezones\n";
}
exit(0);
}
# validate the string if we have DateTime module
elsif ($_HAS_DATETIME_TIMEZONE) {
my $realtz = 0;
foreach ( get_timezone_list() ) {
if ($tz =~ m/^$_$/i) {
$realtz = $_;
last;
}
}
if ($realtz) {
$ENV{TZ} = $realtz;
}
else {
print "invalid timezone provided. Use '-z list' to list valid timezones.\n";
usage();
}
}
# blindly take it otherwise
else {
$ENV{TZ} = $tz;
}
}
else {
print "-z requires the time zone argument\n";
usage();
}
}
else {
print "Unknown option: $_\n";
usage();
}
shift(@ARGV);
}
# Was the time given
if (defined $ARGV[0]) {
my $t_in;
my $t_out;
$TIME = $ARGV[0];
if ($ARGV[0] =~ /\.\./) {
($t_in, $t_out) = split(/\.\./, $ARGV[0]);
}
else {
$t_in = $ARGV[0];
$t_out = 0;
}
$in_seconds = parse_isodate($t_in);
die "Invalid Date: $t_in\n" if ($in_seconds < 0);
if ($t_out) {
$out_seconds = parse_isodate($t_out);
die "Invalid Date: $t_out\n" if ($out_seconds < 0);
}
else {
$out_seconds = 0;
}
}
else {
$in_seconds = 0;
$out_seconds = 0;
}
# Print header info
print_header() if ($header == 1);
# Print the index header
if ($INDEX ne "") {
my $time_str = "";
if ($INDEX_TYPE == $INDEX_DAY) {
$time_str = "Daily";
}
else {
$time_str = "Hourly";
}
if ($BODY ne "") {
print INDEX "$time_str Summary for Timeline of $BODY\n\n";
}
else {
print INDEX "$time_str Summary for Timeline of STDIN\n\n";
}
}
read_body();
print_tl();
################ SUBROUTINES ##################
#convert yyyy-mm-dd string to Unix date
sub parse_isodate {
my $iso_date = shift;
my $sec = 0;
my $min = 0;
my $hour = 0;
my $wday = 0;
my $yday = 0;
if ($iso_date =~ /^(\d\d\d\d)\-(\d\d)\-(\d\d)$/) {
return mktime($sec, $min, $hour, $3, $2 - 1, $1 - 1900, $wday, $yday);
}
elsif ($iso_date =~ /^(\d\d\d\d)\-(\d\d)\-(\d\d)T(\d\d):(\d\d):(\d\d)$/) {
return mktime($6, $5, $4, $3, $2 - 1, $1 - 1900, $wday, $yday);
}
else {
return -1;
}
}
# Read the body file from the BODY variable
sub read_body {
# Read the body file from STDIN or the -b specified body file
if ($BODY ne "") {
open(BODY, "<$BODY") or die "Can't open $BODY";
}
else {
open(BODY, "<&STDIN") or die "Can't dup STDIN";
}
while () {
next if ((/^\#/) || (/^\s+$/));
chomp;
my (
$tmp1, $file, $st_ino, $st_ls,
$st_uid, $st_gid, $st_size, $st_atime,
$st_mtime, $st_ctime, $st_crtime, $tmp2
)
= &tm_split($_);
# Sanity check so that we ignore the header entries
next unless ((defined $st_ino) && ($st_ino =~ /[\d-]+/));
next unless ((defined $st_uid) && ($st_uid =~ /\d+/));
next unless ((defined $st_gid) && ($st_gid =~ /\d+/));
next unless ((defined $st_size) && ($st_size =~ /\d+/));
next unless ((defined $st_mtime) && ($st_mtime =~ /\d+/));
next unless ((defined $st_atime) && ($st_atime =~ /\d+/));
next unless ((defined $st_ctime) && ($st_ctime =~ /\d+/));
next unless ((defined $st_crtime) && ($st_crtime =~ /\d+/));
# we need *some* value in mactimes!
next if (!$st_atime && !$st_mtime && !$st_ctime && !$st_crtime);
# Skip if these are all too early
next
if ( ($st_mtime < $in_seconds)
&& ($st_atime < $in_seconds)
&& ($st_ctime < $in_seconds)
&& ($st_crtime < $in_seconds));
# add leading zeros to timestamps because we will later sort
# these using a string-based comparison
$st_mtime = sprintf("%.10d", $st_mtime);
$st_atime = sprintf("%.10d", $st_atime);
$st_ctime = sprintf("%.10d", $st_ctime);
$st_crtime = sprintf("%.10d", $st_crtime);
# Put all the times in one big array along with the inode and
# name (they are used in the final sorting)
# If the date on the file is too old, don't put it in the array
my $post = ",$st_ino,$file";
if ($out_seconds) {
$timestr2macstr{"$st_mtime$post"} .= "m"
if (
($st_mtime >= $in_seconds)
&& ($st_mtime < $out_seconds)
&& ( (!(exists $timestr2macstr{"$st_mtime$post"}))
|| ($timestr2macstr{"$st_mtime$post"} !~ /m/))
);
$timestr2macstr{"$st_atime$post"} .= "a"
if (
($st_atime >= $in_seconds)
&& ($st_atime < $out_seconds)
&& ( (!(exists $timestr2macstr{"$st_atime$post"}))
|| ($timestr2macstr{"$st_atime$post"} !~ /a/))
);
$timestr2macstr{"$st_ctime$post"} .= "c"
if (
($st_ctime >= $in_seconds)
&& ($st_ctime < $out_seconds)
&& ( (!(exists $timestr2macstr{"$st_ctime$post"}))
|| ($timestr2macstr{"$st_ctime$post"} !~ /c/))
);
$timestr2macstr{"$st_crtime$post"} .= "b"
if (
($st_crtime >= $in_seconds)
&& ($st_crtime < $out_seconds)
&& ( (!(exists $timestr2macstr{"$st_crtime$post"}))
|| ($timestr2macstr{"$st_crtime$post"} !~ /b/))
);
}
else {
$timestr2macstr{"$st_mtime$post"} .= "m"
if (
($st_mtime >= $in_seconds)
&& ( (!(exists $timestr2macstr{"$st_mtime$post"}))
|| ($timestr2macstr{"$st_mtime$post"} !~ /m/))
);
$timestr2macstr{"$st_atime$post"} .= "a"
if (
($st_atime >= $in_seconds)
&& ( (!(exists $timestr2macstr{"$st_atime$post"}))
|| ($timestr2macstr{"$st_atime$post"} !~ /a/))
);
$timestr2macstr{"$st_ctime$post"} .= "c"
if (
($st_ctime >= $in_seconds)
&& ( (!(exists $timestr2macstr{"$st_ctime$post"}))
|| ($timestr2macstr{"$st_ctime$post"} !~ /c/))
);
$timestr2macstr{"$st_crtime$post"} .= "b"
if (
($st_crtime >= $in_seconds)
&& ( (!(exists $timestr2macstr{"$st_crtime$post"}))
|| ($timestr2macstr{"$st_crtime$post"} !~ /b/))
);
}
# if the UID or GID is not in the array then add it.
# these are filled if the -p or -g options are given
$uid2names{$st_uid} = $st_uid
unless (defined $uid2names{$st_uid});
$gid2names{$st_gid} = $st_gid
unless (defined $gid2names{$st_gid});
#
# put /'s between multiple UID/GIDs
#
$uid2names{$st_uid} =~ s@\s@/@g;
$gid2names{$st_gid} =~ s@\s@/@g;
$file2other{$file} =
"$st_ls:$uid2names{$st_uid}:$gid2names{$st_gid}:$st_size";
}
close BODY;
} # end of read_body
sub print_header {
return if ($header == 0);
print "The Sleuth Kit mactime Timeline\n";
print "Input Source: ";
if ($BODY eq "") {
print "STDIN\n";
}
else {
print "$BODY\n";
}
print "Time: $TIME\t\t" if ($TIME ne "");
if ($ENV{TZ} eq "") {
print "\n";
}
else {
print "Timezone: $ENV{TZ}\n";
}
print "passwd File: $PASSWD" if ($PASSWD ne "");
if ($GROUP ne "") {
print "\t" if ($PASSWD ne "");
print "group File: $GROUP";
}
print "\n" if (($PASSWD ne "") || ($GROUP ne ""));
print "\n";
}
#
# Print the time line
#
sub print_tl {
my $prev_day = ""; # has the format of 'day day_week mon year'
my $prev_hour = ""; # has just the hour and is used for hourly index
my $prev_time = 0;
my $prev_cnt = 0;
my $old_date_string = "";
my $delim = ":";
if ($COMMA != 0) {
print "Date,Size,Type,Mode,UID,GID,Meta,File Name\n";
$delim = ",";
}
# Cycle through the files and print them in sorted order.
# Note that we sort using a string comparison because the keys
# also contain the inode and file name
for my $key (sort { $a cmp $b } keys %timestr2macstr) {
my $time;
my $inode;
my $file;
if ($key =~ /^(\d+),([\d-]+),(.*)$/) {
$time = $1;
$inode = $2;
$file = $3;
}
else {
next;
}
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst);
if ($iso8601) {
($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
gmtime($time);
}
else {
($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) =
localtime($time);
}
# the month here is 0-11, not 1-12, like what we want
$mon++;
print
"\t($sec,$min,$hour,MDay: $mday,M: $mon,$year,$wday,$yday,$isdst) = ($time)\n"
if $debug;
#
# cosmetic change to make it look like unix dates
#
$mon = "0$mon" if $mon < 10;
$mday = "0$mday" if $mday < 10;
$hour = "0$hour" if $hour < 10;
$min = "0$min" if $min < 10;
$sec = "0$sec" if $sec < 10;
my $yeart = $year + 1900;
# How do we print the date?
#
my $date_string;
if ($iso8601) {
if ($time == 0) {
$date_string = "0000-00-00T00:00:00Z";
}
else {
$date_string =
"$yeart-$mon-${mday}T$hour:$min:${sec}Z";
}
}
else {
if ($time == 0) {
$date_string = "Xxx Xxx 00 0000 00:00:00";
}
elsif ($month_num) {
$date_string =
"$digit_to_day{$wday} $mon $mday $yeart $hour:$min:$sec";
}
else {
$date_string =
"$digit_to_day{$wday} $digit_to_month{$mon} $mday $yeart $hour:$min:$sec";
}
}
#
# However, we only print the date if it's different from the one
# above. We need to fill the empty space with blanks, though.
#
if ($old_date_string eq $date_string) {
if ($iso8601) {
$date_string = " ";
}
else {
$date_string = " ";
}
$prev_cnt++
if ($INDEX ne "");
}
else {
$old_date_string = $date_string;
# Indexing code
if ($INDEX ne "") {
# First time it is run
if ($prev_day eq "") {
$prev_day = "$mday $wday $mon $yeart";
$prev_hour = $hour;
$prev_time = $time;
$prev_cnt = 0;
}
# A new day, so print the results
elsif ($prev_day ne "$mday $wday $mon $yeart") {
my @prev_vals = split(/ /, $prev_day);
my $date_str;
if ($month_num) {
$date_str =
"$digit_to_day{$prev_vals[1]} "
. "$prev_vals[2] "
. "$prev_vals[0] ${prev_vals[3]}";
}
else {
$date_str =
"$digit_to_day{$prev_vals[1]} "
. "$digit_to_month{$prev_vals[2]} "
. "$prev_vals[0] ${prev_vals[3]}";
}
$date_str .= " $prev_hour:00:00"
if ($INDEX_TYPE == $INDEX_HOUR);
print INDEX "${date_str}${delim} $prev_cnt\n" if ($prev_time > 0);
# Reset
$prev_cnt = 0;
$prev_day = "$mday $wday $mon $yeart";
$prev_hour = $hour;
$prev_time = $time;
}
# Same day, but new hour
elsif (($INDEX_TYPE == $INDEX_HOUR) && ($prev_hour != $hour)) {
my @prev_vals = split(/ /, $prev_day);
if ($month_num) {
print INDEX "$digit_to_day{$prev_vals[1]} "
. "$prev_vals[2] "
. "$prev_vals[0] ${prev_vals[3]} "
. "$prev_hour:00:00${delim} $prev_cnt\n"
if ($prev_time > 0);
}
else {
print INDEX "$digit_to_day{$prev_vals[1]} "
. "$digit_to_month{$prev_vals[2]} "
. "$prev_vals[0] ${prev_vals[3]} "
. "$prev_hour:00:00${delim} $prev_cnt\n"
if ($prev_time > 0);
}
# Reset
$prev_cnt = 0;
$prev_hour = $hour;
$prev_time = $time;
}
$prev_cnt++;
}
}
#
# Muck around with the [mac]times string to make it pretty.
#
my $mactime_tmp = $timestr2macstr{$key};
my $mactime = "";
if ($mactime_tmp =~ /m/) {
$mactime = "m";
}
else {
$mactime = ".";
}
if ($mactime_tmp =~ /a/) {
$mactime .= "a";
}
else {
$mactime .= ".";
}
if ($mactime_tmp =~ /c/) {
$mactime .= "c";
}
else {
$mactime .= ".";
}
if ($mactime_tmp =~ /b/) {
$mactime .= "b";
}
else {
$mactime .= ".";
}
my ($ls, $uids, $groups, $size) = split(/:/, $file2other{$file});
print "FILE: $file MODES: $ls U: $uids G: $groups S: $size\n"
if $debug;
if ($COMMA == 0) {
printf("%s %8s %3s %s %-8s %-8s %-8s %s\n",
$date_string, $size, $mactime, $ls, $uids, $groups, $inode,
$file);
}
else {
# escape any quotes in filename
my $file_tmp = $file;
$file_tmp =~ s/\"/\"\"/g;
printf("%s,%s,%s,%s,%s,%s,%s,\"%s\"\n",
$old_date_string, $size, $mactime, $ls, $uids, $groups, $inode,
$file_tmp);
}
}
# Finish the index page for the last entry
if (($INDEX ne "") && ($prev_cnt > 0)) {
my @prev_vals = split(/ /, $prev_day);
my $date_str;
if ($month_num) {
$date_str =
"$digit_to_day{$prev_vals[1]} "
. "$prev_vals[2] "
. "$prev_vals[0] ${prev_vals[3]}";
}
else {
$date_str =
"$digit_to_day{$prev_vals[1]} "
. "$digit_to_month{$prev_vals[2]} "
. "$prev_vals[0] ${prev_vals[3]}";
}
$date_str .= " $prev_hour:00:00"
if ($INDEX_TYPE == $INDEX_HOUR);
print INDEX "${date_str}${delim} $prev_cnt\n" if ($prev_time > 0);
close INDEX;
}
}
#
# Routines for reading and caching user and group information. These
# are used in multiple programs... it caches the info once, then hopefully
# won't be used again.
#
# Steve Romig, May 1991.
#
# Provides a bunch of routines and a bunch of arrays. Routines
# (and their usage):
#
# load_passwd_info($use_getent, $file_name)
#
# loads user information into the %uname* and %uid* arrays
# (see below).
#
# If $use_getent is non-zero:
# get the info via repeated 'getpwent' calls. This can be
# *slow* on some hosts, especially if they are running as a
# YP (NIS) client.
# If $use_getent is 0:
# if $file_name is "", then get the info from reading the
# results of "ypcat passwd" and from /etc/passwd. Otherwise,
# read the named file. The file should be in passwd(5)
# format.
#
# load_group_info($use_gentent, $file_name)
#
# is similar to load_passwd_info.
#
# Information is stored in several convenient associative arrays:
#
# %uid2names Assoc array, indexed by uid, value is list of
# user names with that uid, in form "name name
# name...".
#
# %gid2members Assoc array, indexed by gid, value is list of
# group members in form "name name name..."
#
# %gname2gid Assoc array, indexed by group name, value is
# matching gid.
#
# %gid2names Assoc array, indexed by gid, value is the
# list of group names with that gid in form
# "name name name...".
#
# You can also use routines named the same as the arrays - pass the index
# as the arg, get back the value. If you use this, get{gr|pw}{uid|gid|nam}
# will be used to lookup entries that aren't found in the cache.
#
# To be done:
# probably ought to add routines to deal with full names.
# maybe there ought to be some anal-retentive checking of password
# and group entries.
# probably ought to cache get{pw|gr}{nam|uid|gid} lookups also.
# probably ought to avoid overwriting existing entries (eg, duplicate
# names in password file would collide in the tables that are
# indexed by name).
#
# Disclaimer:
# If you use YP and you use netgroup entries such as
# +@servers::::::
# +:*:::::/usr/local/utils/messages
# then loading the password file in with &load_passwd_info(0) will get
# you mostly correct YP stuff *except* that it won't do the password and
# shell substitutions as you'd expect. You might want to use
# &load_passwd_info(1) instead to use getpwent calls to do the lookups,
# which would be more correct.
#
#
# minor changes to make it fit with the TCT program, 9/25/99, - dan
# A whole lot removed to clean it up for TSK - July 2008 - Brian
#
package main;
my $passwd_loaded = 0; # flags to use to avoid reloading everything
my $group_loaded = 0; # unnecessarily...
#
# Update user information for the user named $name. We cache the password,
# uid, login group, home directory and shell.
#
sub add_pw_info {
my ($name, $tmp, $uid) = @_;
if ((defined $name) && ($name ne "")) {
if ((defined $uid) && ($uid ne "")) {
if (defined($uid2names{$uid})) {
$uid2names{$uid} .= " $name";
}
else {
$uid2names{$uid} = $name;
}
}
}
}
#
# Update group information for the group named $name. We cache the gid
# and the list of group members.
#
sub add_gr_info {
my ($name, $tmp, $gid) = @_;
if ((defined $name) && ($name ne "")) {
if ((defined $gid) && ($gid ne "")) {
if (defined($gid2names{$gid})) {
$gid2names{$gid} .= " $name";
}
else {
$gid2names{$gid} = $name;
}
}
}
}
sub load_passwd_info {
my ($file_name) = @_;
my (@pw_info);
if ($passwd_loaded) {
return;
}
$passwd_loaded = 1;
open(FILE, $file_name)
|| die "can't open $file_name";
while () {
chop;
if ($_ !~ /^\+/) {
&add_pw_info(split(/:/));
}
}
close(FILE);
}
sub load_group_info {
my ($file_name) = @_;
my (@gr_info);
if ($group_loaded) {
return;
}
$group_loaded = 1;
open(FILE, $file_name)
|| die "can't open $file_name";
while () {
chop;
if ($_ !~ /^\+/) {
&add_gr_info(split(/:/));
}
}
close(FILE);
}
#
# Split a time machine record.
#
sub tm_split {
my ($line) = @_;
my (@fields);
for (@fields = split(/\|/, $line)) {
s/%([A-F0-9][A-F0-9])/pack("C", hex($1))/egis;
}
return @fields;
}
1;
================================================
FILE: tools/sleuthkit-4.12.1-win32/lib/libtsk.lib
================================================
[File too large to display: 52.2 MB]
================================================
FILE: tools/sleuthkit-4.12.1-win32/licenses/IBM-LICENSE
================================================
IBM PUBLIC LICENSE VERSION 1.0 - CORONER TOOLKIT UTILITIES
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS IBM PUBLIC
LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE
PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
1. DEFINITIONS
"Contribution" means:
a) in the case of International Business Machines Corporation ("IBM"),
the Original Program, and
b) in the case of each Contributor,
i) changes to the Program, and
ii) additions to the Program;
where such changes and/or additions to the Program originate
from and are distributed by that particular Contributor.
A Contribution 'originates' from a Contributor if it was added
to the Program by such Contributor itself or anyone acting on
such Contributor's behalf.
Contributions do not include additions to the Program which:
(i) are separate modules of software distributed in conjunction
with the Program under their own license agreement, and
(ii) are not derivative works of the Program.
"Contributor" means IBM and any other entity that distributes the Program.
"Licensed Patents " mean patent claims licensable by a Contributor which
are necessarily infringed by the use or sale of its Contribution alone
or when combined with the Program.
"Original Program" means the original version of the software accompanying
this Agreement as released by IBM, including source code, object code
and documentation, if any.
"Program" means the Original Program and Contributions.
"Recipient" means anyone who receives the Program under this Agreement,
including all Contributors.
2. GRANT OF RIGHTS
a) Subject to the terms of this Agreement, each Contributor hereby
grants Recipient a non-exclusive, worldwide, royalty-free copyright
license to reproduce, prepare derivative works of, publicly display,
publicly perform, distribute and sublicense the Contribution of such
Contributor, if any, and such derivative works, in source code and
object code form.
b) Subject to the terms of this Agreement, each Contributor hereby
grants Recipient a non-exclusive, worldwide, royalty-free patent
license under Licensed Patents to make, use, sell, offer to sell,
import and otherwise transfer the Contribution of such Contributor,
if any, in source code and object code form. This patent license
shall apply to the combination of the Contribution and the Program
if, at the time the Contribution is added by the Contributor, such
addition of the Contribution causes such combination to be covered
by the Licensed Patents. The patent license shall not apply to any
other combinations which include the Contribution. No hardware per
se is licensed hereunder.
c) Recipient understands that although each Contributor grants the
licenses to its Contributions set forth herein, no assurances are
provided by any Contributor that the Program does not infringe the
patent or other intellectual property rights of any other entity.
Each Contributor disclaims any liability to Recipient for claims
brought by any other entity based on infringement of intellectual
property rights or otherwise. As a condition to exercising the rights
and licenses granted hereunder, each Recipient hereby assumes sole
responsibility to secure any other intellectual property rights
needed, if any. For example, if a third party patent license
is required to allow Recipient to distribute the Program, it is
Recipient's responsibility to acquire that license before distributing
the Program.
d) Each Contributor represents that to its knowledge it has sufficient
copyright rights in its Contribution, if any, to grant the copyright
license set forth in this Agreement.
3. REQUIREMENTS
A Contributor may choose to distribute the Program in object code form
under its own license agreement, provided that:
a) it complies with the terms and conditions of this Agreement; and
b) its license agreement:
i) effectively disclaims on behalf of all Contributors all
warranties and conditions, express and implied, including
warranties or conditions of title and non-infringement, and
implied warranties or conditions of merchantability and fitness
for a particular purpose;
ii) effectively excludes on behalf of all Contributors all
liability for damages, including direct, indirect, special,
incidental and consequential damages, such as lost profits;
iii) states that any provisions which differ from this Agreement
are offered by that Contributor alone and not by any other
party; and
iv) states that source code for the Program is available from
such Contributor, and informs licensees how to obtain it in a
reasonable manner on or through a medium customarily used for
software exchange.
When the Program is made available in source code form:
a) it must be made available under this Agreement; and
b) a copy of this Agreement must be included with each copy of the
Program.
Each Contributor must include the following in a conspicuous location
in the Program:
Copyright (c) 1997,1998,1999, International Business Machines
Corporation and others. All Rights Reserved.
In addition, each Contributor must identify itself as the originator of
its Contribution, if any, in a manner that reasonably allows subsequent
Recipients to identify the originator of the Contribution.
4. COMMERCIAL DISTRIBUTION
Commercial distributors of software may accept certain responsibilities
with respect to end users, business partners and the like. While this
license is intended to facilitate the commercial use of the Program, the
Contributor who includes the Program in a commercial product offering
should do so in a manner which does not create potential liability for
other Contributors. Therefore, if a Contributor includes the Program in
a commercial product offering, such Contributor ("Commercial Contributor")
hereby agrees to defend and indemnify every other Contributor
("Indemnified Contributor") against any losses, damages and costs
(collectively "Losses") arising from claims, lawsuits and other legal
actions brought by a third party against the Indemnified Contributor to
the extent caused by the acts or omissions of such Commercial Contributor
in connection with its distribution of the Program in a commercial
product offering. The obligations in this section do not apply to any
claims or Losses relating to any actual or alleged intellectual property
infringement. In order to qualify, an Indemnified Contributor must:
a) promptly notify the Commercial Contributor in writing of such claim,
and
b) allow the Commercial Contributor to control, and cooperate with
the Commercial Contributor in, the defense and any related
settlement negotiations. The Indemnified Contributor may
participate in any such claim at its own expense.
For example, a Contributor might include the Program in a commercial
product offering, Product X. That Contributor is then a Commercial
Contributor. If that Commercial Contributor then makes performance
claims, or offers warranties related to Product X, those performance
claims and warranties are such Commercial Contributor's responsibility
alone. Under this section, the Commercial Contributor would have to
defend claims against the other Contributors related to those performance
claims and warranties, and if a court requires any other Contributor to
pay any damages as a result, the Commercial Contributor must pay those
damages.
5. NO WARRANTY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED
ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR
CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A
PARTICULAR PURPOSE. Each Recipient is solely responsible for determining
the appropriateness of using and distributing the Program and assumes
all risks associated with its exercise of rights under this Agreement,
including but not limited to the risks and costs of program errors,
compliance with applicable laws, damage to or loss of data, programs or
equipment, and unavailability or interruption of operations.
6. DISCLAIMER OF LIABILITY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR
ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION
OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
7. GENERAL
If any provision of this Agreement is invalid or unenforceable under
applicable law, it shall not affect the validity or enforceability of
the remainder of the terms of this Agreement, and without further action
by the parties hereto, such provision shall be reformed to the minimum
extent necessary to make such provision valid and enforceable.
If Recipient institutes patent litigation against a Contributor with
respect to a patent applicable to software (including a cross-claim or
counterclaim in a lawsuit), then any patent licenses granted by that
Contributor to such Recipient under this Agreement shall terminate
as of the date such litigation is filed. In addition, If Recipient
institutes patent litigation against any entity (including a cross-claim
or counterclaim in a lawsuit) alleging that the Program itself (excluding
combinations of the Program with other software or hardware) infringes
such Recipient's patent(s), then such Recipient's rights granted under
Section 2(b) shall terminate as of the date such litigation is filed.
All Recipient's rights under this Agreement shall terminate if it fails
to comply with any of the material terms or conditions of this Agreement
and does not cure such failure in a reasonable period of time after
becoming aware of such noncompliance. If all Recipient's rights under
this Agreement terminate, Recipient agrees to cease use and distribution
of the Program as soon as reasonably practicable. However, Recipient's
obligations under this Agreement and any licenses granted by Recipient
relating to the Program shall continue and survive.
IBM may publish new versions (including revisions) of this Agreement
from time to time. Each new version of the Agreement will be given a
distinguishing version number. The Program (including Contributions)
may always be distributed subject to the version of the Agreement under
which it was received. In addition, after a new version of the Agreement
is published, Contributor may elect to distribute the Program (including
its Contributions) under the new version. No one other than IBM has the
right to modify this Agreement. Except as expressly stated in Sections
2(a) and 2(b) above, Recipient receives no rights or licenses to the
intellectual property of any Contributor under this Agreement, whether
expressly, by implication, estoppel or otherwise. All rights in the
Program not expressly granted under this Agreement are reserved.
This Agreement is governed by the laws of the State of New York and the
intellectual property laws of the United States of America. No party to
this Agreement will bring a legal action under this Agreement more than
one year after the cause of action arose. Each party waives its rights
to a jury trial in any resulting litigation.
================================================
FILE: tools/sleuthkit-4.12.1-win32/licenses/cpl1.0.txt
================================================
Common Public License Version 1.0
THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS COMMON PUBLIC
LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
1. DEFINITIONS
"Contribution" means:
a) in the case of the initial Contributor, the initial code and
documentation distributed under this Agreement, and
b) in the case of each subsequent Contributor:
i) changes to the Program, and
ii) additions to the Program;
where such changes and/or additions to the Program originate from and are
distributed by that particular Contributor. A Contribution 'originates' from a
Contributor if it was added to the Program by such Contributor itself or anyone
acting on such Contributor's behalf. Contributions do not include additions to
the Program which: (i) are separate modules of software distributed in
conjunction with the Program under their own license agreement, and (ii) are not
derivative works of the Program.
"Contributor" means any person or entity that distributes the Program.
"Licensed Patents " mean patent claims licensable by a Contributor which are
necessarily infringed by the use or sale of its Contribution alone or when
combined with the Program.
"Program" means the Contributions distributed in accordance with this Agreement.
"Recipient" means anyone who receives the Program under this Agreement,
including all Contributors.
2. GRANT OF RIGHTS
a) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free copyright license to
reproduce, prepare derivative works of, publicly display, publicly perform,
distribute and sublicense the Contribution of such Contributor, if any, and such
derivative works, in source code and object code form.
b) Subject to the terms of this Agreement, each Contributor hereby grants
Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed
Patents to make, use, sell, offer to sell, import and otherwise transfer the
Contribution of such Contributor, if any, in source code and object code form.
This patent license shall apply to the combination of the Contribution and the
Program if, at the time the Contribution is added by the Contributor, such
addition of the Contribution causes such combination to be covered by the
Licensed Patents. The patent license shall not apply to any other combinations
which include the Contribution. No hardware per se is licensed hereunder.
c) Recipient understands that although each Contributor grants the licenses
to its Contributions set forth herein, no assurances are provided by any
Contributor that the Program does not infringe the patent or other intellectual
property rights of any other entity. Each Contributor disclaims any liability to
Recipient for claims brought by any other entity based on infringement of
intellectual property rights or otherwise. As a condition to exercising the
rights and licenses granted hereunder, each Recipient hereby assumes sole
responsibility to secure any other intellectual property rights needed, if any.
For example, if a third party patent license is required to allow Recipient to
distribute the Program, it is Recipient's responsibility to acquire that license
before distributing the Program.
d) Each Contributor represents that to its knowledge it has sufficient
copyright rights in its Contribution, if any, to grant the copyright license set
forth in this Agreement.
3. REQUIREMENTS
A Contributor may choose to distribute the Program in object code form under its
own license agreement, provided that:
a) it complies with the terms and conditions of this Agreement; and
b) its license agreement:
i) effectively disclaims on behalf of all Contributors all warranties and
conditions, express and implied, including warranties or conditions of title and
non-infringement, and implied warranties or conditions of merchantability and
fitness for a particular purpose;
ii) effectively excludes on behalf of all Contributors all liability for
damages, including direct, indirect, special, incidental and consequential
damages, such as lost profits;
iii) states that any provisions which differ from this Agreement are offered
by that Contributor alone and not by any other party; and
iv) states that source code for the Program is available from such
Contributor, and informs licensees how to obtain it in a reasonable manner on or
through a medium customarily used for software exchange.
When the Program is made available in source code form:
a) it must be made available under this Agreement; and
b) a copy of this Agreement must be included with each copy of the Program.
Contributors may not remove or alter any copyright notices contained within the
Program.
Each Contributor must identify itself as the originator of its Contribution, if
any, in a manner that reasonably allows subsequent Recipients to identify the
originator of the Contribution.
4. COMMERCIAL DISTRIBUTION
Commercial distributors of software may accept certain responsibilities with
respect to end users, business partners and the like. While this license is
intended to facilitate the commercial use of the Program, the Contributor who
includes the Program in a commercial product offering should do so in a manner
which does not create potential liability for other Contributors. Therefore, if
a Contributor includes the Program in a commercial product offering, such
Contributor ("Commercial Contributor") hereby agrees to defend and indemnify
every other Contributor ("Indemnified Contributor") against any losses, damages
and costs (collectively "Losses") arising from claims, lawsuits and other legal
actions brought by a third party against the Indemnified Contributor to the
extent caused by the acts or omissions of such Commercial Contributor in
connection with its distribution of the Program in a commercial product
offering. The obligations in this section do not apply to any claims or Losses
relating to any actual or alleged intellectual property infringement. In order
to qualify, an Indemnified Contributor must: a) promptly notify the Commercial
Contributor in writing of such claim, and b) allow the Commercial Contributor to
control, and cooperate with the Commercial Contributor in, the defense and any
related settlement negotiations. The Indemnified Contributor may participate in
any such claim at its own expense.
For example, a Contributor might include the Program in a commercial product
offering, Product X. That Contributor is then a Commercial Contributor. If that
Commercial Contributor then makes performance claims, or offers warranties
related to Product X, those performance claims and warranties are such
Commercial Contributor's responsibility alone. Under this section, the
Commercial Contributor would have to defend claims against the other
Contributors related to those performance claims and warranties, and if a court
requires any other Contributor to pay any damages as a result, the Commercial
Contributor must pay those damages.
5. NO WARRANTY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE,
NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each
Recipient is solely responsible for determining the appropriateness of using and
distributing the Program and assumes all risks associated with its exercise of
rights under this Agreement, including but not limited to the risks and costs of
program errors, compliance with applicable laws, damage to or loss of data,
programs or equipment, and unavailability or interruption of operations.
6. DISCLAIMER OF LIABILITY
EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS
GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
7. GENERAL
If any provision of this Agreement is invalid or unenforceable under applicable
law, it shall not affect the validity or enforceability of the remainder of the
terms of this Agreement, and without further action by the parties hereto, such
provision shall be reformed to the minimum extent necessary to make such
provision valid and enforceable.
If Recipient institutes patent litigation against a Contributor with respect to
a patent applicable to software (including a cross-claim or counterclaim in a
lawsuit), then any patent licenses granted by that Contributor to such Recipient
under this Agreement shall terminate as of the date such litigation is filed. In
addition, if Recipient institutes patent litigation against any entity
(including a cross-claim or counterclaim in a lawsuit) alleging that the Program
itself (excluding combinations of the Program with other software or hardware)
infringes such Recipient's patent(s), then such Recipient's rights granted under
Section 2(b) shall terminate as of the date such litigation is filed.
All Recipient's rights under this Agreement shall terminate if it fails to
comply with any of the material terms or conditions of this Agreement and does
not cure such failure in a reasonable period of time after becoming aware of
such noncompliance. If all Recipient's rights under this Agreement terminate,
Recipient agrees to cease use and distribution of the Program as soon as
reasonably practicable. However, Recipient's obligations under this Agreement
and any licenses granted by Recipient relating to the Program shall continue and
survive.
Everyone is permitted to copy and distribute copies of this Agreement, but in
order to avoid inconsistency the Agreement is copyrighted and may only be
modified in the following manner. The Agreement Steward reserves the right to
publish new versions (including revisions) of this Agreement from time to time.
No one other than the Agreement Steward has the right to modify this Agreement.
IBM is the initial Agreement Steward. IBM may assign the responsibility to serve
as the Agreement Steward to a suitable separate entity. Each new version of the
Agreement will be given a distinguishing version number. The Program (including
Contributions) may always be distributed subject to the version of the Agreement
under which it was received. In addition, after a new version of the Agreement
is published, Contributor may elect to distribute the Program (including its
Contributions) under the new version. Except as expressly stated in Sections
2(a) and 2(b) above, Recipient receives no rights or licenses to the
intellectual property of any Contributor under this Agreement, whether
expressly, by implication, estoppel or otherwise. All rights in the Program not
expressly granted under this Agreement are reserved.
This Agreement is governed by the laws of the State of New York and the
intellectual property laws of the United States of America. No party to this
Agreement will bring a legal action under this Agreement more than one year
after the cause of action arose. Each party waives its rights to a jury trial in
any resulting litigation.