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.

TRACE Logo

## 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)


TRACE Preview


## 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 🗂️


Registry Browser

### File Carving 🔪


File Carving

### File Search 🔍


Image Verification

### Image Verification ✅


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** 🍏 | macOS Screenshot | | **Kali Linux 2024** 🐧 | Kali Linux Screenshot | | **\*WSL2 - Ubuntu 22.04.3 LTS** 🐧 | Kali Linux Screenshot | | **Windows 10** 🗔 | Windows Screenshot | ## 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) [![LinkedIn](https://img.shields.io/badge/LinkedIn-%230077B5.svg?logo=linkedin&logoColor=white)](https://linkedin.com/in/radoslav-gadzhovski)
![Version](https://img.shields.io/badge/version-1.2.0-purple.svg) ![License](https://img.shields.io/badge/license-MIT-green.svg) ================================================ 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"" exif_table += "
{key}{value}
" 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"" extended_metadata += f"" extended_metadata += f"" extended_metadata += f"" # Add disk offset for carved files if is_carved: offset_value = data.get('offset', 0) extended_metadata += f"" extended_metadata += f"" extended_metadata += f"" extended_metadata += f"" extended_metadata += f"" extended_metadata += f"" extended_metadata += f"" extended_metadata += f"
Name:{data.get('name', 'N/A')}
Type:{data.get('type')}
MIME Type:{mime_type}
Size:{size}
Disk Offset:{hex(offset_value)} ({offset_value} bytes)
Modified:{modified_time}
Accessed:{accessed_time}
Created:{created_time}
Changed:{changed_time}
MD5:{md5_hash}
SHA-256:{sha256_hash}
" 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:

{scan_rows}
Antivirus Detected Version Last Update Result
""" 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 ================================================ [![Build Status](https://travis-ci.org/sleuthkit/sleuthkit.svg?branch=develop)](https://travis-ci.org/sleuthkit/sleuthkit) [![Build status](https://ci.appveyor.com/api/projects/status/8f7ljj8s2lh5sqfv?svg=true)](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.