This commit is contained in:
Der Benji
2026-06-09 14:17:19 +02:00
parent cf85adb6a9
commit 8589857b86
12 changed files with 5409 additions and 2063 deletions
+233 -16
View File
@@ -1,7 +1,8 @@
# Transportable Detonation Chamber
A pre-configured Windows 11 VM for malware detonation testing against multiple EDR solutions.
Uses Vagrant + Hyper-V to provision a fully automated analysis environment.
Supports **Windows hosts** (Hyper-V) and **macOS Apple Silicon hosts** (QEMU/UTM) with
architecture-aware provisioning.
## What's Inside
@@ -17,7 +18,7 @@ Uses Vagrant + Hyper-V to provision a fully automated analysis environment.
```
┌──────────────────────────────────────────────────────────────────────────┐
│ Windows 11 VM (Hyper-V) │
│ Windows 11 VM (Hyper-V or QEMU/UTM) │
│ │
│ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Detonator │───▶│ DetonatorAgent │ │ LitterBox │ │
@@ -40,28 +41,114 @@ Uses Vagrant + Hyper-V to provision a fully automated analysis environment.
│ │ │ │
│ ▼ ▼ │
│ Windows Event Log NDJSON alerts │
│ (JSON format) (C:\tools\rustinel\alerts) │
│ (JSON format) (C:\tools\rustinel\logs) │
└──────────────────────────────────────────────────────────────────────────┘
```
## Platform Support
| Host OS | Hypervisor | Guest Arch | Vagrantfile | Performance |
|---------|-----------|------------|-------------|-------------|
| Windows 10/11 (x86_64) | Hyper-V | x86_64 | `Vagrantfile` | Native (fastest) |
| macOS Apple Silicon (M1-M4) | QEMU via vagrant-qemu | ARM64 | `Vagrantfile.utm` | Near-native via hvf |
### How It Works on Each Platform
**Windows Host (Hyper-V)**
- Uses the standard `Vagrantfile` with the `hyperv` provider
- Guest runs Windows 11 x86_64 natively on Hyper-V
- All tools (Fibratus, Rustinel, Sysmon, .NET, Python) run as native x86_64 binaries
- Port forwarding handled by Hyper-V virtual switch
- Box: `gusztavvargadr/windows-11` from Vagrant Cloud (auto-downloaded)
**macOS Host (UTM/QEMU)**
- Uses `Vagrantfile.utm` with the `vagrant-qemu` provider
- Guest runs Windows 11 ARM64 under Apple's Hypervisor.framework (hvf)
- Architecture-aware provisioning detects `$env:PROCESSOR_ARCHITECTURE -eq "ARM64"`:
- **Sysmon**: Native ARM64 binary (`Sysmon64a.exe` from the same Sysmon.zip)
- **Fibratus**: x86_64 binary under Windows ARM emulation (no ARM64 build available)
- **Rustinel**: x86_64 binary under Windows ARM emulation (no ARM64 build available)
- **.NET 8 / Python 3.12**: Native ARM64 (full support)
- **DetonatorAgent**: Builds natively for ARM64 via .NET 8
- Windows ARM's emulation layer runs x86_64 tools transparently with ~10-20% overhead
- ETW kernel tracing works under emulation (kernel itself is native ARM64)
## Prerequisites
- **Windows 10/11 host** with Hyper-V enabled
### Windows Host (Hyper-V)
- **Windows 10/11** with Hyper-V enabled
- **Vagrant** >= 2.4 ([download](https://www.vagrantup.com/downloads))
- **Administrator** PowerShell (required for Hyper-V)
- ~30 GB free disk space
- ~8 GB RAM available for the VM
### Enable Hyper-V
```powershell
# Run as Administrator
# Enable Hyper-V (run as Administrator, reboot required)
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All
# Reboot required
```
### macOS Host (Apple Silicon - UTM/QEMU)
- **macOS** on Apple Silicon (M1, M2, M3, M4)
- **Homebrew**: install from https://brew.sh
- **QEMU**: `brew install qemu`
- **Vagrant**: `brew install --cask vagrant`
- **vagrant-qemu plugin**: `vagrant plugin install vagrant-qemu`
- **Windows 11 ARM64 Vagrant box** (see below)
- ~80 GB free disk space
- ~8 GB RAM available for the VM
**Obtaining the Windows 11 ARM64 box:**
There is no official Windows 11 ARM64 box on Vagrant Cloud. You need to create one:
*Option A - Build with Packer (recommended):*
```bash
# Download Windows 11 ARM64 ISO from Microsoft:
# https://www.microsoft.com/software-download/windows11arm64
# Use a Packer template for ARM64
git clone https://github.com/StefanScherer/packer-windows
cd packer-windows
# Follow ARM64 build instructions in the repo, output: windows11-arm.box
# Import the box
vagrant box add win11-arm output/windows11-arm.box --provider qemu
```
*Option B - Convert from existing UTM/QCOW2 VM:*
```bash
# 1. Create a Windows 11 ARM VM manually in UTM
# 2. Inside the VM, configure WinRM for Vagrant (elevated PowerShell):
winrm quickconfig -force
winrm set winrm/config/service '@{AllowUnencrypted="true"}'
winrm set winrm/config/service/auth '@{Basic="true"}'
net user vagrant vagrant /add
net localgroup Administrators vagrant /add
# 3. Shut down the VM, locate the .qcow2 disk image
# 4. Package into a Vagrant box:
mkdir box-build && cd box-build
cat > metadata.json << 'EOF'
{"provider": "qemu"}
EOF
cp /path/to/disk.qcow2 box-disk.qcow2
tar czf win11-arm.box metadata.json box-disk.qcow2 Vagrantfile
# 5. Import:
vagrant box add win11-arm win11-arm.box --provider qemu
```
*Option C - Community box (check availability):*
```bash
vagrant cloud search windows-11-arm --provider qemu
```
## Quick Start
### Windows (Hyper-V)
```powershell
# Clone this repo
git clone https://github.com/your-user/transportable-detonation-chamber.git
@@ -73,6 +160,26 @@ vagrant up --provider=hyperv
# The first boot takes ~20-30 minutes (downloads + installs)
```
### macOS Apple Silicon (QEMU)
```bash
# Clone this repo
git clone https://github.com/your-user/transportable-detonation-chamber.git
cd transportable-detonation-chamber
# Use the UTM Vagrantfile
cp Vagrantfile.utm Vagrantfile.local
export VAGRANT_VAGRANTFILE=Vagrantfile.utm
# Start the VM
vagrant up --provider=qemu
# The first boot takes ~30-45 minutes (ARM emulation + downloads)
```
> **Note**: On macOS, you can also symlink the Vagrantfile:
> `ln -sf Vagrantfile.utm Vagrantfile` (then just use `vagrant up`).
Once provisioning completes, the services start automatically:
- **Unified Web UI**: http://localhost:9000 (recommended - integrates everything)
@@ -136,6 +243,8 @@ Invoke-RestMethod -Uri "http://localhost:8080/api/lock/status"
## VM Management
### Common Commands (both platforms)
```powershell
# Stop the VM
vagrant halt
@@ -154,7 +263,8 @@ vagrant provision
# Destroy and rebuild from scratch
vagrant destroy -f
vagrant up --provider=hyperv
vagrant up --provider=hyperv # Windows
vagrant up --provider=qemu # macOS (with VAGRANT_VAGRANTFILE=Vagrantfile.utm)
# Take a snapshot (recommended before detonation)
vagrant snapshot save clean_state
@@ -163,6 +273,23 @@ vagrant snapshot save clean_state
vagrant snapshot restore clean_state
```
### macOS-specific Notes
```bash
# Set the UTM Vagrantfile persistently
export VAGRANT_VAGRANTFILE=Vagrantfile.utm
# Or symlink for convenience
ln -sf Vagrantfile.utm Vagrantfile
# If vagrant-qemu hangs on boot, increase the timeout:
# Edit Vagrantfile.utm and set config.vm.boot_timeout = 1800
# Connect via RDP (install Microsoft Remote Desktop from App Store)
vagrant rdp
# Or manually: open rdp://localhost:3389
```
## Configuration
### Custom Fibratus rules
@@ -204,22 +331,25 @@ Set-MpPreference -DisableRealtimeMonitoring $true
```
transportable-detonation-chamber/
├── Vagrantfile # VM definition
├── Vagrantfile # VM definition (Windows host, Hyper-V)
├── Vagrantfile.utm # VM definition (macOS Apple Silicon, QEMU)
├── config/
│ ├── fibratus.yml # Fibratus EDR config (JSON eventlog output)
│ ├── rustinel-config.toml # Rustinel EDR config
│ ├── rustinel-config.toml # Rustinel EDR config (Sigma/YARA/IOC paths)
│ └── profiles_init.yaml # Detonator target profiles
├── webui/ # Unified web interface
│ ├── app.py # Flask backend (API aggregation)
│ ├── app.py # Flask backend (API aggregation, alert loading)
│ ├── requirements.txt
│ ├── templates/index.html # SPA shell
│ └── static/
│ ├── css/style.css # Dark theme (Rustinel-inspired)
│ └── js/app.js # Frontend logic
│ └── js/app.js # Frontend logic (process tree, detail panels)
├── scripts/
│ ├── install-prerequisites.ps1 # .NET 8, Python, Git, Chocolatey
│ ├── install-fibratus.ps1 # Fibratus v3.0.0
│ ├── install-rustinel.ps1 # Rustinel v1.1.1
│ ├── install-prerequisites.ps1 # .NET 8, Python 3.12, Git, Chocolatey, 7-Zip
│ ├── install-sysmon.ps1 # Sysmon (ARM64-aware: Sysmon64a.exe)
│ ├── install-fibratus.ps1 # Fibratus v3.0.0 (ARM64 emulation warning)
│ ├── install-rustinel.ps1 # Rustinel v1.1.1 (ARM64 emulation warning)
│ ├── install-detection-rules.ps1 # Sigma + YARA rules (rustinel-rules + Elastic)
│ ├── install-detonator.ps1 # Detonator + DetonatorAgent from source
│ ├── install-litterbox.ps1 # LitterBox payload analysis sandbox
│ ├── install-webui.ps1 # Unified web UI
@@ -275,6 +405,49 @@ curl http://localhost:8080/api/lock/status
Get-Content C:\tools\logs\DetonatorAgent.log -Tail 50
```
### macOS/UTM: QEMU won't start
```bash
# Verify QEMU is installed and supports hvf
qemu-system-aarch64 --accel help
# Should show: hvf
# Check that the EFI firmware exists
ls /opt/homebrew/share/qemu/edk2-aarch64-code.fd
# If missing: brew reinstall qemu
# Check vagrant-qemu plugin is installed
vagrant plugin list | grep qemu
```
### macOS/UTM: VM boots but WinRM times out
The Windows 11 ARM64 box must have WinRM configured:
```powershell
# Inside the VM (via UTM console or manual RDP):
winrm quickconfig -force
Set-Item WSMan:\localhost\Service\AllowUnencrypted -Value true
Set-Item WSMan:\localhost\Service\Auth\Basic -Value true
New-NetFirewallRule -Name "WinRM" -DisplayName "WinRM" -Protocol TCP -LocalPort 5985 -Action Allow
```
### Rustinel not detecting events
```powershell
# Check Rustinel is running
Get-Process rustinel
# Check Rustinel log for ETW errors
Get-Content C:\tools\rustinel\logs\rustinel.log.* | Select-Object -Last 20
# Verify ETW trace session
logman query -ets | findstr rustinel
# If the ETW session is stale, stop and let Rustinel recreate it:
logman stop rustinel-etw-trace -ets
Start-ScheduledTask -TaskName "Rustinel"
```
## Security Notes
- This VM is designed for **malware analysis** - treat it as compromised
@@ -283,6 +456,50 @@ Get-Content C:\tools\logs\DetonatorAgent.log -Tail 50
- Defender exclusions are configured for detonation paths only
- Rustinel active response is **disabled by default** - enable after testing
## ARM64 Limitations (macOS/UTM)
When running on Apple Silicon via QEMU:
| Component | ARM64 Support | Notes |
|-----------|--------------|-------|
| Sysmon | Native | `Sysmon64a.exe` included in Sysmon.zip |
| Fibratus | Emulated (x86_64) | No ARM64 build; MSI installs under emulation |
| Rustinel | Emulated (x86_64) | No ARM64 build; ETW works under emulation |
| .NET 8 | Native | Full ARM64 SDK and runtime |
| Python 3.12 | Native | ARM64 installer from python.org |
| DetonatorAgent | Native | Compiled from source via .NET 8 |
| Detonator/LitterBox | Native | Python-based, runs on ARM64 Python |
**Known ARM64 caveats:**
- First launch of emulated x86_64 binaries is slower (JIT compilation of emulation)
- Fibratus kernel driver may have reduced functionality under emulation
- Some YARA rules that scan PE sections may behave differently for ARM64 PEs
- Total provisioning time is ~30-45 min vs ~20-30 min on native x86_64
## Detection Rules
The VM ships with a curated detection ruleset installed by `install-detection-rules.ps1`:
**Sigma Rules (20 rules from `Karib0u/rustinel-rules` windows-advanced pack):**
- 14 process_creation rules (encoded PowerShell, schtasks, LOLBins, credential dumping)
- 3 registry_event rules (Run key persistence, Defender tampering, WDigest)
- 1 task_creation rule (suspicious scheduled task actions)
- 1 ps_script rule (PowerShell script block logging)
- 1 service_creation rule
**YARA Rules (717 compiled rules):**
- Rustinel-rules pack: malware family signatures
- Elastic protections-artifacts: threat detection rules from Elastic Security
**IOC Engine:**
- Hash matching (MD5/SHA1/SHA256)
- Hot-reload: add IOCs at runtime, rules refresh within 2 seconds
Rules are loaded from:
- Sigma: `C:\tools\detection-rules\rustinel-rules\dist\windows-advanced\rules\sigma\`
- YARA: `C:\tools\detection-rules\yara-combined\` (junction combining both sources)
- IOC: `C:\tools\detection-rules\rustinel-rules\dist\windows-advanced\rules\ioc\`
## Credits
- [dobin/detonator](https://github.com/dobin/detonator) - Orchestration framework
+143
View File
@@ -0,0 +1,143 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Transportable Detonation Chamber - UTM / QEMU (Apple Silicon)
# Windows 11 ARM VM with Detonator + DetonatorAgent + Fibratus + Rustinel + LitterBox
#
# This Vagrantfile targets macOS hosts with Apple Silicon (M1/M2/M3/M4)
# using the vagrant-qemu provider and a Windows 11 ARM64 guest.
#
# Prerequisites:
# - macOS on Apple Silicon
# - Homebrew: brew install qemu
# - Vagrant: brew install --cask vagrant
# - Plugin: vagrant plugin install vagrant-qemu
# - A Windows 11 ARM64 Vagrant box (see below)
#
# Setting up the Windows 11 ARM box:
# Option A - Use a pre-built community box (if available):
# vagrant box add win11-arm path/to/windows11-arm.box --provider qemu
#
# Option B - Build from ISO using Packer:
# 1. Download Windows 11 ARM64 ISO from:
# https://www.microsoft.com/software-download/windows11arm64
# 2. Use packer template from: https://github.com/StefanScherer/packer-windows
# (ARM64 variant)
# 3. Import: vagrant box add win11-arm output/windows11-arm.box --provider qemu
#
# Option C - Convert a UTM/QCOW2 image:
# 1. Create Windows 11 ARM VM in UTM manually
# 2. Install WinRM: winrm quickconfig -force
# 3. Set vagrant/vagrant credentials
# 4. Package: vagrant package --base <vm-name> --output win11-arm.box
#
# Usage:
# vagrant up --provider=qemu
# vagrant rdp
# vagrant halt
# vagrant destroy
Vagrant.configure("2") do |config|
# Windows 11 ARM64 box - adjust name to match your imported box
config.vm.box = "win11-arm"
config.vm.hostname = "detonation-chamber"
# Communicator settings for Windows
config.vm.communicator = "winrm"
config.winrm.username = "vagrant"
config.winrm.password = "vagrant"
config.winrm.timeout = 1800
config.winrm.retry_limit = 30
# Network: expose service ports
config.vm.network "forwarded_port", guest: 5000, host: 5000 # Detonator Web UI
config.vm.network "forwarded_port", guest: 8000, host: 8000 # Detonator REST API
config.vm.network "forwarded_port", guest: 8080, host: 8080 # DetonatorAgent API
config.vm.network "forwarded_port", guest: 1337, host: 1337 # LitterBox Web UI
config.vm.network "forwarded_port", guest: 9000, host: 9000 # Unified Web UI
# QEMU provider settings (vagrant-qemu plugin)
config.vm.provider "qemu" do |qe|
qe.arch = "aarch64"
qe.machine = "virt,highmem=on"
qe.cpu = "host"
qe.smp = "cpus=4,sockets=1,cores=4,threads=1"
qe.memory = "4G"
qe.net_device = "virtio-net-pci"
qe.ssh_port = 50222
# Enable Apple Hypervisor.framework acceleration (native speed)
qe.accel = "hvf"
# Disk: use virtio-blk for best performance
qe.drive_interface = "virtio"
qe.disk_size = "80G"
# EFI boot (required for Windows 11 ARM)
qe.extra_qemu_args = %w(
-bios /opt/homebrew/share/qemu/edk2-aarch64-code.fd
-device virtio-gpu-pci
-device qemu-xhci
-device usb-kbd
-device usb-tablet
)
end
# Increase boot timeout for Windows
config.vm.boot_timeout = 1200
# Disable default synced folder
config.vm.synced_folder ".", "/vagrant", disabled: true
# Copy config and webui into the VM via file provisioners
config.vm.provision "file", source: "config", destination: "C:\\vagrant_config"
config.vm.provision "file", source: "webui", destination: "C:\\vagrant\\webui"
# Provisioning: run scripts in order
# All scripts are architecture-aware (detect ARM64 vs x86_64 automatically)
config.vm.provision "prerequisites",
type: "shell",
path: "scripts/install-prerequisites.ps1",
privileged: true
config.vm.provision "sysmon",
type: "shell",
path: "scripts/install-sysmon.ps1",
privileged: true
config.vm.provision "fibratus",
type: "shell",
path: "scripts/install-fibratus.ps1",
privileged: true
config.vm.provision "rustinel",
type: "shell",
path: "scripts/install-rustinel.ps1",
privileged: true
config.vm.provision "detection-rules",
type: "shell",
path: "scripts/install-detection-rules.ps1",
privileged: true
config.vm.provision "detonator",
type: "shell",
path: "scripts/install-detonator.ps1",
privileged: true
config.vm.provision "litterbox",
type: "shell",
path: "scripts/install-litterbox.ps1",
privileged: true
config.vm.provision "webui",
type: "shell",
path: "scripts/install-webui.ps1",
privileged: true
config.vm.provision "configure",
type: "shell",
path: "scripts/configure-services.ps1",
privileged: true,
run: "always"
end
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

+29 -11
View File
@@ -84,6 +84,7 @@ Add-MpPreference -ExclusionPath $infectedDir -ErrorAction SilentlyContinue
Add-MpPreference -ExclusionPath "C:\samples" -ErrorAction SilentlyContinue
Add-MpPreference -ExclusionPath "C:\Users\Public\Downloads" -ErrorAction SilentlyContinue
Add-MpPreference -ExclusionPath "C:\LitterBox" -ErrorAction SilentlyContinue
Add-MpPreference -ExclusionPath "C:\tools\detection-rules" -ErrorAction SilentlyContinue
Write-Host "[+] Sample directories created and excluded from Defender" -ForegroundColor Green
Write-Host " Desktop\infected: $infectedDir" -ForegroundColor Gray
Write-Host " Samples: C:\samples" -ForegroundColor Gray
@@ -137,20 +138,26 @@ if (Test-Path $rustinelExe) {
Copy-Item "C:\vagrant_config\rustinel-config.toml" "$rustinelDir\config.toml" -Force
}
# Rustinel needs a PowerShell wrapper (CMD pipe redirection causes early exit)
# Stop existing Rustinel process
Stop-Process -Name "rustinel" -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
# Run Rustinel directly as a scheduled task (no wrapper needed)
# WorkingDirectory must be set so it finds config.toml and writes to logs/ dir
# Uses "run" subcommand for foreground mode (task keeps it alive)
Unregister-ScheduledTask -TaskName "Rustinel" -Confirm:$false -ErrorAction SilentlyContinue
$ps1Content = @"
Start-Process -FilePath "$rustinelExe" -ArgumentList "run" -WorkingDirectory "$rustinelDir" -WindowStyle Hidden -RedirectStandardOutput "$logsDir\Rustinel-stdout.log" -RedirectStandardError "$logsDir\Rustinel-stderr.log" -Wait
"@
Set-Content -Path "$logsDir\run-Rustinel.ps1" -Value $ps1Content
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File $logsDir\run-Rustinel.ps1" -WorkingDirectory $rustinelDir
$action = New-ScheduledTaskAction -Execute $rustinelExe -Argument "run" -WorkingDirectory $rustinelDir
$trigger = New-ScheduledTaskTrigger -AtStartup
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ([TimeSpan]::Zero)
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit (New-TimeSpan -Days 365)
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName "Rustinel" -Action $action -Trigger $trigger -Settings $settings -Principal $principal | Out-Null
Start-ScheduledTask -TaskName "Rustinel"
Start-Sleep -Seconds 3
Write-Host "[+] Rustinel registered and started" -ForegroundColor Green
Start-Sleep -Seconds 5
if (Get-Process -Name "rustinel" -ErrorAction SilentlyContinue) {
Write-Host "[+] Rustinel registered and running (ETW trace active)" -ForegroundColor Green
} else {
Write-Host "[!] Rustinel task started but process not detected" -ForegroundColor Yellow
}
} else {
Write-Host "[!] Rustinel not found at $rustinelExe - skipping" -ForegroundColor Yellow
}
@@ -161,8 +168,19 @@ $agentExe = "$detonatorAgentDir\publish\DetonatorAgent.exe"
$agentDll = "$detonatorAgentDir\publish\DetonatorAgent.dll"
if (Test-Path $agentExe) {
Register-ServiceTask -Name "DetonatorAgent" -Command $agentExe -Arguments "--port 8080 --edr fibratus" -WorkingDirectory "$detonatorAgentDir\publish"
Start-Sleep -Seconds 3
# DetonatorAgent runs directly (no CMD wrapper needed for .NET apps)
Stop-Process -Name "DetonatorAgent" -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
Unregister-ScheduledTask -TaskName "DetonatorAgent" -Confirm:$false -ErrorAction SilentlyContinue
$action = New-ScheduledTaskAction -Execute $agentExe -Argument "--port 8080 --edr fibratus" -WorkingDirectory "$detonatorAgentDir\publish"
$trigger = New-ScheduledTaskTrigger -AtStartup
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit (New-TimeSpan -Days 365)
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName "DetonatorAgent" -Action $action -Trigger $trigger -Settings $settings -Principal $principal | Out-Null
Start-ScheduledTask -TaskName "DetonatorAgent"
Start-Sleep -Seconds 5
# Firewall rule for the exe
New-NetFirewallRule -DisplayName "DetonatorAgent EXE" -Direction Inbound -Protocol TCP -Program $agentExe -Action Allow -Profile Any -ErrorAction SilentlyContinue | Out-Null
} elseif (Test-Path $agentDll) {
$dotnetExe = (Get-Command dotnet -ErrorAction SilentlyContinue).Source
if ($dotnetExe) {
+8
View File
@@ -14,6 +14,14 @@ Set-StrictMode -Version Latest
Write-Host "=== Installing Fibratus ===" -ForegroundColor Cyan
# Architecture detection
$isArm64 = ($env:PROCESSOR_ARCHITECTURE -eq "ARM64")
if ($isArm64) {
Write-Host "[!] ARM64 detected - Fibratus has no native ARM64 build" -ForegroundColor Yellow
Write-Host "[*] Installing x86_64 build (runs under Windows ARM emulation layer)" -ForegroundColor Yellow
Write-Host "[*] Note: ETW kernel tracing may have limitations under emulation" -ForegroundColor Yellow
}
$fibratusVersion = "3.0.0"
$fibratusInstallDir = "$env:ProgramFiles\Fibratus"
$fibratusMsiUrl = "https://github.com/rabbitstack/fibratus/releases/download/v${fibratusVersion}/fibratus-${fibratusVersion}-amd64.msi"
+8
View File
@@ -16,6 +16,14 @@ Set-StrictMode -Version Latest
Write-Host "=== Installing Rustinel ===" -ForegroundColor Cyan
# Architecture detection
$isArm64 = ($env:PROCESSOR_ARCHITECTURE -eq "ARM64")
if ($isArm64) {
Write-Host "[!] ARM64 detected - Rustinel has no native ARM64 build" -ForegroundColor Yellow
Write-Host "[*] Installing x86_64 build (runs under Windows ARM emulation layer)" -ForegroundColor Yellow
Write-Host "[*] Note: ETW tracing should work under emulation but with slight overhead" -ForegroundColor Yellow
}
$rustinelVersion = "1.1.1"
$rustinelInstallDir = "C:\tools\rustinel"
$rustinelZipUrl = "https://github.com/Karib0u/rustinel/releases/download/v${rustinelVersion}/rustinel-${rustinelVersion}-x86_64-pc-windows-msvc.zip"
+35 -8
View File
@@ -22,12 +22,25 @@ Set-StrictMode -Version Latest
Write-Host "=== Installing Sysmon ===" -ForegroundColor Cyan
# Detect architecture - ARM64 uses Sysmon64a.exe, x86_64 uses Sysmon64.exe
$isArm64 = ($env:PROCESSOR_ARCHITECTURE -eq "ARM64")
$sysmonDir = "C:\tools\sysmon"
if ($isArm64) {
$sysmonExe = "$sysmonDir\Sysmon64a.exe"
$sysmonServiceName = "Sysmon64a"
Write-Host "[*] ARM64 detected - will use Sysmon64a.exe (native ARM64 build)" -ForegroundColor Yellow
} else {
$sysmonExe = "$sysmonDir\Sysmon64.exe"
$sysmonServiceName = "Sysmon64"
}
$configPath = "$sysmonDir\sysmonconfig.xml"
# Check if already installed and running
$sysmonSvc = Get-Service -Name $sysmonServiceName -ErrorAction SilentlyContinue
if (-not $sysmonSvc) {
# Also check alternate service name (ARM64 may register as Sysmon64)
$sysmonSvc = Get-Service -Name "Sysmon64" -ErrorAction SilentlyContinue
}
if ($sysmonSvc -and $sysmonSvc.Status -eq "Running") {
Write-Host "[+] Sysmon already installed and running" -ForegroundColor Green
Write-Host "[*] Updating configuration..." -ForegroundColor Yellow
@@ -72,11 +85,21 @@ Expand-Archive -Path $sysmonZipPath -DestinationPath $sysmonDir -Force
Remove-Item $sysmonZipPath -Force -ErrorAction SilentlyContinue
if (-not (Test-Path $sysmonExe)) {
Write-Host "[!] Sysmon64.exe not found after extraction" -ForegroundColor Red
# Check for alternate name
if (Test-Path "$sysmonDir\sysmon64.exe") {
$sysmonExe = "$sysmonDir\sysmon64.exe"
} else {
Write-Host "[!] $([System.IO.Path]::GetFileName($sysmonExe)) not found after extraction" -ForegroundColor Red
# Check for alternate names
$candidates = @("$sysmonDir\Sysmon64a.exe", "$sysmonDir\Sysmon64.exe", "$sysmonDir\sysmon64.exe", "$sysmonDir\sysmon64a.exe")
$found = $false
foreach ($candidate in $candidates) {
if (Test-Path $candidate) {
$sysmonExe = $candidate
$found = $true
Write-Host "[*] Using $candidate" -ForegroundColor Yellow
break
}
}
if (-not $found) {
Write-Host "[!] No Sysmon binary found. Contents:" -ForegroundColor Red
Get-ChildItem $sysmonDir | ForEach-Object { Write-Host " $_" }
exit 1
}
}
@@ -126,14 +149,18 @@ Write-Host "[*] Installing Sysmon service..." -ForegroundColor Yellow
# Verify installation
Start-Sleep -Seconds 3
$sysmonSvc = Get-Service -Name $sysmonServiceName -ErrorAction SilentlyContinue
if (-not $sysmonSvc) {
$sysmonSvc = Get-Service -Name "Sysmon64" -ErrorAction SilentlyContinue
}
if ($sysmonSvc -and $sysmonSvc.Status -eq "Running") {
Write-Host "[+] Sysmon installed and running" -ForegroundColor Green
Write-Host "[+] Sysmon installed and running (service: $($sysmonSvc.Name))" -ForegroundColor Green
} else {
# Try starting it
Start-Service -Name "Sysmon64" -ErrorAction SilentlyContinue
$svcName = if (Get-Service -Name $sysmonServiceName -ErrorAction SilentlyContinue) { $sysmonServiceName } else { "Sysmon64" }
Start-Service -Name $svcName -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
$sysmonSvc = Get-Service -Name "Sysmon64" -ErrorAction SilentlyContinue
$sysmonSvc = Get-Service -Name $svcName -ErrorAction SilentlyContinue
if ($sysmonSvc -and $sysmonSvc.Status -eq "Running") {
Write-Host "[+] Sysmon installed and started" -ForegroundColor Green
} else {
+483 -15
View File
@@ -27,6 +27,7 @@ DETONATOR_API = os.environ.get("DETONATOR_API", "http://127.0.0.1:8000")
DETONATOR_AGENT_API = os.environ.get("DETONATOR_AGENT_API", "http://127.0.0.1:8080")
LITTERBOX_API = os.environ.get("LITTERBOX_API", "http://127.0.0.1:1337")
WEBUI_PORT = int(os.environ.get("WEBUI_PORT", "9000"))
SUBMISSIONS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "submissions.json")
# In-memory event store (populated from Rustinel NDJSON + Fibratus)
events_store = {
@@ -42,6 +43,69 @@ events_store = {
}
store_lock = threading.Lock()
# --- Submissions History ---
submissions_lock = threading.Lock()
def _load_submissions():
"""Load submission history from JSON file."""
if os.path.isfile(SUBMISSIONS_FILE):
try:
with open(SUBMISSIONS_FILE, "r") as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
return []
return []
def _save_submissions(submissions):
"""Persist submission history to JSON file."""
try:
with open(SUBMISSIONS_FILE, "w") as f:
json.dump(submissions, f, indent=2)
except IOError:
pass
def _record_submission(filename, sha256, size, target, results):
"""Record a new submission in the history."""
import datetime
entry = {
"id": hashlib.md5(f"{sha256}{time.time()}".encode()).hexdigest()[:12],
"timestamp": datetime.datetime.now().isoformat(),
"filename": filename,
"sha256": sha256,
"size": size,
"target": target,
"agent_status": None,
"agent_pid": None,
"litterbox_status": None,
"file_path": None,
}
# Extract results
if "agent" in results:
entry["agent_status"] = "success" if 200 <= results["agent"].get("status", 0) < 400 else "failed"
agent_data = results["agent"].get("data")
if isinstance(agent_data, dict):
if agent_data.get("pid"):
entry["agent_pid"] = agent_data["pid"]
entry["file_path"] = agent_data.get("file_path") or agent_data.get("path")
if "litterbox" in results:
entry["litterbox_status"] = "success" if 200 <= results["litterbox"].get("status", 0) < 400 else "failed"
# If no file_path from agent, guess the common location
if not entry["file_path"]:
entry["file_path"] = f"C:\\Users\\vagrant\\Desktop\\infected\\{filename}"
with submissions_lock:
subs = _load_submissions()
subs.insert(0, entry) # newest first
# Keep max 200 entries
subs = subs[:200]
_save_submissions(subs)
return entry
# --- Rustinel NDJSON Parser ---
def _get_nested(data, dotted_key, default=None):
@@ -141,8 +205,241 @@ def load_rustinel_alerts():
return unique_alerts
# --- Fibratus Event Log Ingestion ---
# Fibratus writes alerts to Windows Event Log: Application log, Provider "Fibratus", JSON format.
_fibratus_last_read_time = None # Track last read timestamp to avoid re-reading
def parse_fibratus_alert(data):
"""Parse a Fibratus JSON alert into the normalized alert format."""
if not isinstance(data, dict):
return None
alert_id = data.get("id") or hashlib.sha256(json.dumps(data, sort_keys=True).encode()).hexdigest()[:16]
# Get first event (Fibratus alerts contain an array of triggering events)
events = data.get("events", [])
first_event = events[0] if events else {}
proc = first_event.get("proc", {})
# Map Fibratus category to ECS-like category
fibratus_cat = first_event.get("category", "").lower()
category_map = {
"process": "process",
"file": "file",
"registry": "registry",
"net": "network",
"network": "network",
"image": "process",
"thread": "process",
"dns": "dns",
}
category = category_map.get(fibratus_cat, fibratus_cat)
# Extract MITRE tags from labels if present
tags = []
labels = data.get("labels", {})
for key, val in labels.items():
if "mitre" in key.lower() or "attack" in key.lower():
if isinstance(val, list):
tags.extend(val)
elif isinstance(val, str):
tags.append(val)
# Also check for tags in the title/text for common MITRE patterns
title = data.get("title", "")
alert = {
"id": f"fib_{alert_id}",
"timestamp": first_event.get("timestamp", ""),
"severity": data.get("severity", "unknown").lower(),
"rule_name": title or "Fibratus Detection",
"rule_description": data.get("description", "") or data.get("text", ""),
"engine": "fibratus",
"tags": tags,
"category": category,
"pid": proc.get("pid"),
"process_name": proc.get("name", ""),
"process_image": proc.get("exe", ""),
"command_line": proc.get("cmdline", ""),
"parent_pid": proc.get("ppid"),
"parent_name": proc.get("parent_name", ""),
"parent_command_line": proc.get("parent_cmdline", ""),
"user": proc.get("username", ""),
"detonated": True,
"detonation_source": "fibratus",
"raw": data,
}
return alert
def load_fibratus_alerts():
"""Load Fibratus alerts from Windows Event Log (Application log, Provider: Fibratus)."""
global _fibratus_last_read_time
alerts = []
# Build PowerShell command to query Fibratus events
# Use Get-WinEvent with FilterHashtable for efficiency
ps_cmd = (
"Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName='Fibratus'} "
"-MaxEvents 500 -ErrorAction SilentlyContinue | "
"ForEach-Object { $_.Message } "
)
try:
result = subprocess.run(
["powershell", "-NoProfile", "-Command", ps_cmd],
capture_output=True, text=True, timeout=10,
creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0,
)
if result.returncode != 0 or not result.stdout.strip():
return alerts
# Each event message is a JSON blob; they may be separated by newlines
# PowerShell outputs each Message on its own line(s)
raw_output = result.stdout.strip()
# Try to split by JSON object boundaries
# Fibratus JSON alerts start with { and end with }
depth = 0
current_json = []
for line in raw_output.split("\n"):
line = line.rstrip()
if not line:
continue
current_json.append(line)
depth += line.count("{") - line.count("}")
if depth <= 0 and current_json:
json_str = "\n".join(current_json)
current_json = []
depth = 0
try:
data = json.loads(json_str)
alert = parse_fibratus_alert(data)
if alert:
alerts.append(alert)
except json.JSONDecodeError:
continue
# Handle any remaining buffer
if current_json:
json_str = "\n".join(current_json)
try:
data = json.loads(json_str)
alert = parse_fibratus_alert(data)
if alert:
alerts.append(alert)
except json.JSONDecodeError:
pass
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
print(f"[fibratus_loader] Error reading event log: {e}")
return alerts
return alerts
def load_litterbox_results():
"""Poll LitterBox API for completed analysis results and convert to alert format."""
alerts = []
try:
r = requests.get(f"{LITTERBOX_API}/api/analyses", params={"status": "completed"}, timeout=5)
if r.status_code != 200:
return alerts
analyses = r.json() if isinstance(r.json(), list) else r.json().get("results", [])
except (requests.RequestException, ValueError):
return alerts
for analysis in analyses:
try:
analysis_id = analysis.get("id") or analysis.get("task_id", "")
score = analysis.get("score", 0) or analysis.get("threat_score", 0)
sample = analysis.get("sample", {}) or {}
filename = sample.get("name") or analysis.get("filename", "unknown")
sha256 = sample.get("sha256") or analysis.get("sha256", "")
started = analysis.get("started") or analysis.get("timestamp", "")
completed = analysis.get("completed") or started
# Only create alert-level entries for analyses with findings
if score <= 0:
continue
# Map score to severity
if score >= 8:
severity = "critical"
elif score >= 5:
severity = "high"
elif score >= 3:
severity = "medium"
else:
severity = "low"
# Get process info from behavioral analysis if available
behaviors = analysis.get("behaviors", []) or analysis.get("signatures", [])
proc_name = analysis.get("process_name", "")
proc_pid = analysis.get("pid")
proc_image = analysis.get("process_image", "")
cmdline = analysis.get("command_line", "")
# Try to extract from first behavior if top-level is empty
if not proc_name and behaviors:
first_b = behaviors[0] if isinstance(behaviors[0], dict) else {}
proc_name = first_b.get("process_name", "")
proc_pid = first_b.get("pid") or proc_pid
proc_image = first_b.get("process_image", "") or proc_image
# Build description from signatures/behaviors
sigs = []
for b in (behaviors[:5] if behaviors else []):
if isinstance(b, dict):
sigs.append(b.get("name") or b.get("description", ""))
elif isinstance(b, str):
sigs.append(b)
description = "; ".join(s for s in sigs if s) if sigs else f"LitterBox analysis score: {score}/10"
# Extract tags (MITRE, etc)
tags = analysis.get("tags", []) or []
mitre = analysis.get("mitre_attacks", []) or analysis.get("ttps", [])
if mitre:
tags.extend([t.get("technique_id", t) if isinstance(t, dict) else str(t) for t in mitre])
alert = {
"id": f"lb_{analysis_id}",
"timestamp": completed,
"severity": severity,
"rule_name": f"LitterBox: {filename}",
"rule_description": description,
"engine": "litterbox",
"tags": tags,
"category": "process",
"pid": proc_pid,
"process_name": proc_name or filename,
"process_image": proc_image,
"command_line": cmdline,
"parent_pid": None,
"parent_name": "",
"parent_command_line": "",
"user": analysis.get("user", ""),
"detonated": True,
"detonation_source": "litterbox",
"litterbox_score": score,
"litterbox_id": analysis_id,
"sha256": sha256,
"raw": analysis,
}
alerts.append(alert)
except (KeyError, TypeError, ValueError):
continue
return alerts
def build_process_tree(alerts):
"""Build process tree from alerts data."""
"""Build process tree from alerts data.
Handles PID reuse: if a PID's executable changes between alerts,
use the most recent process info (latest alert wins).
"""
processes = {}
for alert in alerts:
pid = alert.get("pid")
@@ -166,24 +463,48 @@ def build_process_tree(alerts):
"file": 0, "network": 0, "dns": 0, "http": 0,
"registry": 0, "modules": 0, "scripts": 0, "injection": 0,
"wmi": 0, "services": 0, "tasks": 0, "logons": 0,
"artifacts": 0, "threats": 0,
"artifacts": 0, "threats": 0, "detonated": 0,
},
"alerts": [],
"detonated": False,
"detonation_sources": [],
}
if pid:
# Handle PID reuse: if the executable changed, update process identity
# (later alerts overwrite older ones so the most recent process info wins)
proc_entry = processes.get(pid)
if proc_entry:
alert_image = alert.get("process_image", "")
alert_ts = alert.get("timestamp", "")
if alert_image and alert_image != proc_entry["image"]:
# Different executable on same PID = PID reuse; update to latest
if alert_ts >= (proc_entry.get("last_seen") or ""):
proc_entry["name"] = alert.get("process_name", proc_entry["name"])
proc_entry["image"] = alert_image
proc_entry["command_line"] = alert.get("command_line") or proc_entry["command_line"]
proc_entry["user"] = alert.get("user") or proc_entry["user"]
if alert.get("parent_pid"):
proc_entry["parent_pid"] = alert.get("parent_pid")
proc_entry["parent_name"] = alert.get("parent_name", "")
proc_entry["alerts"].append(alert)
# Count activity by category
cat = alert.get("category", "")
if isinstance(cat, list):
cat = cat[0] if cat else ""
cat_lower = cat.lower()
proc_entry = processes.get(pid)
if proc_entry:
proc_entry["alerts"].append(alert)
proc_entry["activity"]["threats"] += 1
if "file" in cat_lower:
proc_entry["activity"]["file"] += 1
elif "network" in cat_lower:
proc_entry["activity"]["network"] += 1
# Check if HTTP specifically (ports 80/443/8080/8443)
raw = alert.get("raw", {})
dest_port = _get_nested(raw, "destination.port") or _get_nested(raw, "network.destination.port")
if dest_port in (80, 443, 8080, 8443, "80", "443", "8080", "8443"):
proc_entry["activity"]["http"] += 1
elif "dns" in cat_lower:
proc_entry["activity"]["dns"] += 1
elif "registry" in cat_lower:
@@ -191,6 +512,19 @@ def build_process_tree(alerts):
elif "process" in cat_lower:
proc_entry["activity"]["modules"] += 1
# Count artifacts (YARA/IOC matches)
engine = alert.get("engine", "").lower()
if engine in ("yara", "ioc"):
proc_entry["activity"]["artifacts"] += 1
# Track detonation enrichment (Fibratus / LitterBox)
if alert.get("detonated"):
proc_entry["activity"]["detonated"] += 1
proc_entry["detonated"] = True
det_src = alert.get("detonation_source", "")
if det_src and det_src not in proc_entry["detonation_sources"]:
proc_entry["detonation_sources"].append(det_src)
# Track last seen timestamp
ts = alert.get("timestamp", "")
if ts and ts > (proc_entry.get("last_seen") or ""):
@@ -232,13 +566,32 @@ def build_process_tree(alerts):
# --- Background alert loader ---
def alert_loader_thread():
"""Periodically reload alerts from Rustinel."""
"""Periodically reload alerts from Rustinel, Fibratus, and LitterBox."""
while True:
try:
alerts = load_rustinel_alerts()
processes = build_process_tree(alerts)
# Load from all sources
rustinel_alerts = load_rustinel_alerts()
fibratus_alerts = load_fibratus_alerts()
litterbox_alerts = load_litterbox_results()
# Merge and deduplicate
all_alerts = rustinel_alerts + fibratus_alerts + litterbox_alerts
seen = set()
unique_alerts = []
for alert in all_alerts:
aid = alert.get("id")
if aid and aid not in seen:
seen.add(aid)
unique_alerts.append(alert)
elif not aid:
unique_alerts.append(alert)
# Sort by timestamp
unique_alerts.sort(key=lambda a: a.get("timestamp", ""))
processes = build_process_tree(unique_alerts)
with store_lock:
events_store["alerts"] = alerts
events_store["alerts"] = unique_alerts
events_store["processes"] = processes
except Exception as e:
print(f"[alert_loader] Error: {e}")
@@ -462,16 +815,19 @@ def api_alerts():
"""Get all Rustinel/Fibratus alerts."""
with store_lock:
alerts = events_store.get("alerts", [])
# Filter by severity/engine if requested
# Filter by severity/engine/detonated if requested
severity = request.args.get("severity")
engine = request.args.get("engine")
pid = request.args.get("pid", type=int)
since = request.args.get("since") # ISO timestamp - only alerts after this time
detonated = request.args.get("detonated")
if severity:
alerts = [a for a in alerts if a.get("severity", "").lower() == severity.lower()]
if engine:
alerts = [a for a in alerts if a.get("engine", "").lower() == engine.lower()]
if detonated and detonated.lower() in ("true", "1", "yes"):
alerts = [a for a in alerts if a.get("detonated")]
if pid:
alerts = [a for a in alerts if a.get("pid") == pid]
if since:
@@ -799,9 +1155,20 @@ def api_submit():
"sha256": file_sha256,
}
# Record submission in history
_record_submission(filename, file_sha256, len(file_bytes), target, results)
return jsonify(results)
@app.route("/api/submissions")
def api_submissions():
"""Return submission history list."""
with submissions_lock:
subs = _load_submissions()
return jsonify(subs)
def _add_hash_to_ioc(sha256_hash, filename=""):
"""Add a file hash to Rustinel's IOC hash feed for real-time detection."""
# Try multiple possible IOC paths
@@ -867,9 +1234,10 @@ def api_file_download():
@app.route("/api/file/hex")
def api_file_hex():
"""Return hex dump of a file's first N bytes."""
"""Return hex dump of a file's bytes starting from a given offset."""
filepath = request.args.get("path", "")
num_bytes = min(request.args.get("bytes", 8192, type=int), 65536)
start_offset = max(request.args.get("offset", 0, type=int), 0)
if not filepath:
return jsonify({"error": "No path specified"}), 400
@@ -879,27 +1247,127 @@ def api_file_hex():
return jsonify({"error": "File not found", "hex": ""}), 404
try:
file_size = os.path.getsize(norm_path)
with open(norm_path, "rb") as f:
f.seek(start_offset)
data = f.read(num_bytes)
# Generate hex dump
lines = []
for offset in range(0, len(data), 16):
chunk = data[offset:offset + 16]
for line_offset in range(0, len(data), 16):
chunk = data[line_offset:line_offset + 16]
hex_part = " ".join(f"{b:02x}" for b in chunk[:8])
hex_part += " " + " ".join(f"{b:02x}" for b in chunk[8:])
ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
lines.append(f"{offset:08x} {hex_part:<49s} |{ascii_part}|")
abs_offset = start_offset + line_offset
lines.append(f"{abs_offset:08x} {hex_part:<49s} |{ascii_part}|")
# Also provide raw bytes as list for the data inspector
raw_bytes = list(data)
return jsonify({
"hex": "\n".join(lines),
"size": os.path.getsize(norm_path),
"size": file_size,
"bytes_shown": len(data),
"offset": start_offset,
"raw_bytes": raw_bytes,
})
except (IOError, OSError) as e:
return jsonify({"error": str(e), "hex": ""}), 500
@app.route("/api/file/hex/upload", methods=["POST"])
def api_file_hex_upload():
"""Accept a file upload, save to temp, return hex dump + path for further pagination."""
if "file" not in request.files:
return jsonify({"error": "No file provided"}), 400
file = request.files["file"]
filename = file.filename or "uploaded_file"
file_bytes = file.read()
# Save to a temp directory for subsequent pagination requests
import tempfile
hex_temp_dir = os.path.join(tempfile.gettempdir(), "hex_uploads")
os.makedirs(hex_temp_dir, exist_ok=True)
# Use hash-based name to avoid conflicts but keep extension
file_hash = hashlib.sha256(file_bytes).hexdigest()[:16]
safe_name = "".join(c for c in filename if c.isalnum() or c in ".-_")[:80]
dest_path = os.path.join(hex_temp_dir, f"{file_hash}_{safe_name}")
with open(dest_path, "wb") as f:
f.write(file_bytes)
# Generate initial hex dump
num_bytes = min(request.form.get("bytes", 512, type=int), 65536)
lines = []
for line_offset in range(0, min(len(file_bytes), num_bytes), 16):
chunk = file_bytes[line_offset:line_offset + 16]
hex_part = " ".join(f"{b:02x}" for b in chunk[:8])
hex_part += " " + " ".join(f"{b:02x}" for b in chunk[8:])
ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
lines.append(f"{line_offset:08x} {hex_part:<49s} |{ascii_part}|")
return jsonify({
"hex": "\n".join(lines),
"size": len(file_bytes),
"bytes_shown": min(len(file_bytes), num_bytes),
"offset": 0,
"raw_bytes": list(file_bytes[:num_bytes]),
"path": dest_path,
"filename": filename,
})
@app.route("/api/file/hex/write", methods=["POST"])
def api_file_hex_write():
"""Write modified bytes back to a file at a specific offset (hex editor save)."""
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "No JSON body provided"}), 400
filepath = data.get("path", "")
offset = data.get("offset", 0)
byte_values = data.get("bytes", []) # List of int values 0-255
if not filepath:
return jsonify({"error": "No path specified"}), 400
if not byte_values:
return jsonify({"error": "No bytes to write"}), 400
norm_path = os.path.normpath(filepath)
# Security: only allow writes to user-writable directories
allowed_write_prefixes = [
r"C:\Users",
r"C:\Temp",
r"C:\Windows\Temp",
]
if not any(norm_path.startswith(prefix) for prefix in allowed_write_prefixes):
return jsonify({"error": "Access denied: write not allowed to this path"}), 403
if not os.path.isfile(norm_path):
return jsonify({"error": "File not found"}), 404
try:
# Validate byte values
raw_bytes = bytes([b & 0xFF for b in byte_values])
with open(norm_path, "r+b") as f:
f.seek(offset)
f.write(raw_bytes)
return jsonify({
"status": "ok",
"path": norm_path,
"offset": offset,
"bytes_written": len(raw_bytes),
})
except (IOError, OSError) as e:
return jsonify({"error": str(e)}), 500
# --- Main ---
if __name__ == "__main__":
# Start background alert loader
+1
View File
@@ -0,0 +1 @@
Python wurde nicht gefunden; ohne Argumente ausführen, um aus dem Microsoft Store zu installieren, oder deaktivieren Sie diese Verknüpfung unter "Einstellungen > Apps > Erweiterte App-Einstellungen > App-Ausführungsaliase".
+1764 -439
View File
File diff suppressed because it is too large Load Diff
+2461 -1555
View File
File diff suppressed because it is too large Load Diff
+245 -20
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Detonation Chamber</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="/static/css/style.css?v=2">
</head>
<body>
<div id="app">
@@ -15,7 +15,9 @@
<nav class="sidebar-tabs">
<button class="tab active" data-tab="dashboard">Dashboard</button>
<button class="tab" data-tab="tracing">Tracing</button>
<button class="tab" data-tab="graph">Graph</button>
<button class="tab" data-tab="sysmon">Sysmon</button>
<button class="tab" data-tab="hexeditor">Hex</button>
<button class="tab" data-tab="submit">Submit</button>
</nav>
</div>
@@ -63,28 +65,173 @@
</div>
</div>
<!-- Tracing Tab -->
<!-- Tracing Tab - RUSTINEL TRACE Analysis Console -->
<div class="tab-content" id="tab-tracing">
<div class="content-header">
<h2>Event Tracing</h2>
<div class="header-actions">
<select id="filter-severity" class="filter-select">
<option value="">All Severities</option>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
<!-- Console Header Bar -->
<div class="rtrace-header">
<div class="rtrace-title">
<span class="rtrace-brand"><span class="rtrace-brand-highlight">RUSTINEL TRACE</span> Analysis Console</span>
</div>
<div class="rtrace-header-actions">
<select id="rtrace-process-select" class="rtrace-process-dropdown">
<option value="">-- select process --</option>
</select>
<select id="filter-engine" class="filter-select">
<option value="">All Engines</option>
<option value="sigma">Sigma</option>
<option value="yara">YARA</option>
<option value="ioc">IOC</option>
</select>
<button class="btn btn-sm" onclick="refreshAlerts()">Refresh</button>
<button class="btn btn-sm" onclick="refreshAlerts()">&#x21BB; Refresh</button>
<button class="btn btn-sm" onclick="clearStoppedProcesses()">&#x23F8; Clear stopped</button>
<button class="btn btn-sm" onclick="clearAllTracing()">&#x23F9; Clear all</button>
</div>
</div>
<div id="alerts-table" class="events-table"></div>
<!-- Process Info Bar -->
<div class="rtrace-info-bar" id="rtrace-info-bar">
<span class="rtrace-proc-name" id="rtrace-proc-name">--</span>
<span class="rtrace-verdict-badge" id="rtrace-verdict-badge">--</span>
<span class="rtrace-tag" id="rtrace-tag-status">--</span>
<span class="rtrace-tag" id="rtrace-tag-platform">windows</span>
<span class="rtrace-stat" id="rtrace-stat-procs">0 processes</span>
<span class="rtrace-stat" id="rtrace-stat-events">0 events</span>
<span class="rtrace-stat" id="rtrace-stat-duration">0m 0s</span>
<span class="rtrace-path" id="rtrace-path">--</span>
</div>
<!-- Severity Summary Bar -->
<div class="rtrace-severity-bar" id="rtrace-severity-bar">
<div class="rtrace-sev-section">
<div class="rtrace-sev-counts" id="rtrace-sev-counts"></div>
</div>
<div class="rtrace-sev-section">
<div class="rtrace-engines" id="rtrace-engines"></div>
</div>
<div class="rtrace-sev-section">
<div class="rtrace-top-rules" id="rtrace-top-rules"></div>
</div>
</div>
<!-- Timeline -->
<div class="rtrace-timeline-section">
<div class="rtrace-timeline-label">
<span>TIMELINE</span>
<span class="rtrace-timeline-time-range" id="rtrace-timeline-range"></span>
</div>
<div class="rtrace-timeline-bar" id="rtrace-timeline-bar">
<div class="rtrace-timeline-cursor" id="rtrace-timeline-cursor"></div>
</div>
<div class="rtrace-timeline-legend">
<span class="rtrace-legend-item"><span class="rtrace-legend-dot" style="background:#ef4444"></span>Critical/High</span>
<span class="rtrace-legend-item"><span class="rtrace-legend-dot" style="background:#3b82f6"></span>Process</span>
<span class="rtrace-legend-item"><span class="rtrace-legend-dot" style="background:#22c55e"></span>Network</span>
<span class="rtrace-legend-item"><span class="rtrace-legend-dot" style="background:#a78bfa"></span>DNS</span>
<span class="rtrace-legend-item"><span class="rtrace-legend-dot" style="background:#f97316"></span>File</span>
<span class="rtrace-legend-item"><span class="rtrace-legend-dot" style="background:#f472b6"></span>Registry</span>
</div>
</div>
<!-- Split View: Process Tree + Detail -->
<div class="rtrace-split">
<!-- Left: Process Tree -->
<div class="rtrace-tree-panel">
<div class="rtrace-tree-header">
<span>PROCESS TREE (<span id="rtrace-tree-count">0</span>) &mdash; TIME FROM START</span>
</div>
<div class="rtrace-tree-list" id="rtrace-tree-list">
<!-- Rendered by JS -->
</div>
</div>
<!-- Right: Detail Tabs + Event Table -->
<div class="rtrace-detail-panel">
<div class="rtrace-detail-placeholder" id="rtrace-detail-placeholder">
Select a process to inspect its details.
</div>
<div class="rtrace-detail-content" id="rtrace-detail-content" style="display:none;">
<!-- Detail tabs -->
<div class="rtrace-detail-tabs" id="rtrace-detail-tabs">
<button class="rtrace-tab active" data-rtab="verdict">Verdict</button>
<button class="rtrace-tab" data-rtab="live">Live</button>
<button class="rtrace-tab" data-rtab="http">HTTP Requests <span class="rtrace-tab-count">0</span></button>
<button class="rtrace-tab" data-rtab="connections">Connections <span class="rtrace-tab-count">0</span></button>
<button class="rtrace-tab" data-rtab="dns">DNS Requests <span class="rtrace-tab-count">0</span></button>
<button class="rtrace-tab" data-rtab="files">Files <span class="rtrace-tab-count">0</span></button>
<button class="rtrace-tab" data-rtab="registry">Registry <span class="rtrace-tab-count">0</span></button>
<button class="rtrace-tab" data-rtab="artifacts">Artifacts <span class="rtrace-tab-count">0</span></button>
<button class="rtrace-tab" data-rtab="modules">Modules <span class="rtrace-tab-count">0</span></button>
</div>
<!-- Event Table -->
<div class="rtrace-event-table-wrapper" id="rtrace-event-table-wrapper">
<!-- Column headers -->
<div class="rtrace-event-table-header">
<div class="rtrace-col rtrace-col-sev">Sev</div>
<div class="rtrace-col rtrace-col-time">Time</div>
<div class="rtrace-col rtrace-col-action">Action</div>
<div class="rtrace-col rtrace-col-rule">Rule / Engine</div>
<div class="rtrace-col rtrace-col-pid">PID</div>
<div class="rtrace-col rtrace-col-process">Process</div>
<div class="rtrace-col rtrace-col-details">Details</div>
</div>
<div class="rtrace-event-table-body" id="rtrace-event-table-body">
<!-- Rendered by JS -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Graph Tab -->
<div class="tab-content" id="tab-graph">
<div class="graph-toolbar">
<div class="graph-toolbar-left">
<span class="graph-title">PROCESS ROLLUP GRAPH</span>
<span class="graph-subtitle" id="graph-node-count">0 nodes, 0 edges</span>
</div>
<div class="graph-toolbar-right">
<div class="graph-search-wrapper">
<input type="text" id="graph-search" class="graph-search-input" placeholder="Search process / filename..." autocomplete="off" spellcheck="false">
<span class="graph-search-icon">&#128269;</span>
<span class="graph-search-clear" id="graph-search-clear">&times;</span>
</div>
<div class="graph-time-filter">
<button class="graph-time-btn" data-seconds="30">30s</button>
<button class="graph-time-btn" data-seconds="60">1m</button>
<button class="graph-time-btn" data-seconds="300">5m</button>
<button class="graph-time-btn" data-seconds="900">15m</button>
<button class="graph-time-btn" data-seconds="3600">1h</button>
<button class="graph-time-btn" data-seconds="86400">24h</button>
<button class="graph-time-btn active" data-seconds="0">All</button>
</div>
<label class="graph-toggle"><input type="checkbox" id="graph-show-network" checked> Network</label>
<label class="graph-toggle"><input type="checkbox" id="graph-show-dns" checked> DNS</label>
<label class="graph-toggle"><input type="checkbox" id="graph-show-files"> Files</label>
<label class="graph-toggle"><input type="checkbox" id="graph-show-registry"> Registry</label>
<label class="graph-toggle detonated"><input type="checkbox" id="graph-show-detonated"> Detonated</label>
<select id="graph-layout" class="filter-select">
<option value="force">Force-directed</option>
<option value="hierarchy" selected>Hierarchical</option>
</select>
<button class="btn btn-sm" onclick="graphFitView()">Fit</button>
<button class="btn btn-sm" onclick="graphRefresh()">Refresh</button>
</div>
</div>
<div class="graph-container" id="graph-container">
<canvas id="graph-canvas"></canvas>
<div class="graph-tooltip" id="graph-tooltip"></div>
<div class="graph-legend">
<span class="graph-legend-item"><span class="graph-legend-dot" style="background:#3b82f6"></span>Process</span>
<span class="graph-legend-item"><span class="graph-legend-dot" style="background:#ef4444"></span>Malicious</span>
<span class="graph-legend-item"><span class="graph-legend-dot detonated-dot" style="background:#fbbf24"></span>Detonated</span>
<span class="graph-legend-item"><span class="graph-legend-dot" style="background:#22c55e"></span>Network</span>
<span class="graph-legend-item"><span class="graph-legend-dot" style="background:#a78bfa"></span>DNS</span>
<span class="graph-legend-item"><span class="graph-legend-dot" style="background:#f97316"></span>File</span>
<span class="graph-legend-item"><span class="graph-legend-dot" style="background:#f472b6"></span>Registry</span>
<span class="graph-legend-item line"><span class="graph-legend-line spawn"></span>Spawned</span>
<span class="graph-legend-item line"><span class="graph-legend-line network"></span>Connection</span>
<span class="graph-legend-item line"><span class="graph-legend-line inject"></span>Injection</span>
</div>
</div>
<div class="graph-detail-panel" id="graph-detail-panel">
<div class="graph-detail-header" id="graph-detail-header">Select a node</div>
<div class="graph-detail-body" id="graph-detail-body"></div>
</div>
</div>
<!-- Sysmon Tab -->
@@ -117,6 +264,74 @@
<div id="sysmon-table" class="events-table"></div>
</div>
<!-- Hex Editor Tab -->
<div class="tab-content" id="tab-hexeditor">
<div class="content-header">
<h2>Hex Editor</h2>
<div class="header-actions">
<input type="number" id="hex-offset" class="filter-input hex-offset-input" placeholder="Offset" value="0" min="0">
<select id="hex-bytes-per-page" class="filter-select">
<option value="256">256 bytes</option>
<option value="512" selected>512 bytes</option>
<option value="1024">1 KB</option>
<option value="4096">4 KB</option>
<option value="8192">8 KB</option>
</select>
<button class="btn btn-sm" onclick="hexPrevPage()">&#x25C0; Prev</button>
<button class="btn btn-sm" onclick="hexNextPage()">Next &#x25B6;</button>
</div>
</div>
<!-- Drop zone + path input -->
<div class="hex-drop-zone" id="hex-drop-zone">
<div class="hex-drop-icon">&#x1F4C2;</div>
<p>Drop a file here or <span class="hex-browse-link" onclick="document.getElementById('hex-file-input').click()">browse</span></p>
<input type="file" id="hex-file-input" hidden>
<div class="hex-path-row">
<span class="hex-path-or">or enter VM path:</span>
<input type="text" id="hex-filepath" class="filter-input hex-path-input" placeholder="C:\path\to\file...">
<button class="btn btn-sm btn-primary" onclick="hexLoad()">Load</button>
</div>
</div>
<div class="hex-editor-container">
<!-- File Info Bar -->
<div class="hex-file-info" id="hex-file-info">
<span class="hex-info-item">No file loaded</span>
</div>
<!-- Hex Editor Body -->
<div class="hex-editor-body">
<!-- Offset Column -->
<div class="hex-offset-col" id="hex-offset-col"></div>
<!-- Hex View -->
<div class="hex-view" id="hex-view"></div>
<!-- ASCII View -->
<div class="hex-ascii-col" id="hex-ascii-col"></div>
</div>
<!-- Hex Status Bar -->
<div class="hex-status-bar" id="hex-status-bar">
<span class="hex-status-item" id="hex-status-offset">Offset: 0x00000000</span>
<span class="hex-status-item" id="hex-status-selection">No selection</span>
<span class="hex-status-item" id="hex-status-value">--</span>
<span class="hex-status-item" id="hex-status-size">Size: --</span>
</div>
<!-- Hex Inspector -->
<div class="hex-inspector" id="hex-inspector">
<div class="hex-inspector-title">DATA INSPECTOR</div>
<div class="hex-inspector-fields" id="hex-inspector-fields">
<div class="hex-insp-row"><span class="hex-insp-label">Int8</span><span class="hex-insp-value" id="hex-insp-int8">--</span></div>
<div class="hex-insp-row"><span class="hex-insp-label">UInt8</span><span class="hex-insp-value" id="hex-insp-uint8">--</span></div>
<div class="hex-insp-row"><span class="hex-insp-label">Int16 LE</span><span class="hex-insp-value" id="hex-insp-int16le">--</span></div>
<div class="hex-insp-row"><span class="hex-insp-label">UInt16 LE</span><span class="hex-insp-value" id="hex-insp-uint16le">--</span></div>
<div class="hex-insp-row"><span class="hex-insp-label">Int32 LE</span><span class="hex-insp-value" id="hex-insp-int32le">--</span></div>
<div class="hex-insp-row"><span class="hex-insp-label">UInt32 LE</span><span class="hex-insp-value" id="hex-insp-uint32le">--</span></div>
<div class="hex-insp-row"><span class="hex-insp-label">Float32</span><span class="hex-insp-value" id="hex-insp-float32">--</span></div>
<div class="hex-insp-row"><span class="hex-insp-label">Float64</span><span class="hex-insp-value" id="hex-insp-float64">--</span></div>
<div class="hex-insp-row"><span class="hex-insp-label">ASCII</span><span class="hex-insp-value" id="hex-insp-ascii">--</span></div>
<div class="hex-insp-row"><span class="hex-insp-label">UTF-16 LE</span><span class="hex-insp-value" id="hex-insp-utf16">--</span></div>
</div>
</div>
</div>
</div>
<!-- Submit Tab -->
<div class="tab-content" id="tab-submit">
<div class="content-header">
@@ -148,6 +363,16 @@
</div>
<div id="submit-result" class="submit-result"></div>
</div>
<!-- Submissions History -->
<div class="submissions-history">
<div class="submissions-header">
<h3>Submission History</h3>
<button class="btn btn-sm" onclick="refreshSubmissions()">Refresh</button>
</div>
<div class="submissions-list" id="submissions-list">
<div style="padding:12px;color:var(--text-muted);font-size:11px;">Loading submissions...</div>
</div>
</div>
</div>
</main>
@@ -168,6 +393,6 @@
</div>
</div>
<script src="/static/js/app.js"></script>
<script src="/static/js/app.js?v=2"></script>
</body>
</html>