mirror of
https://github.com/BenjiTrapp/transportable-detonation-chamber
synced 2026-08-09 12:01:14 +00:00
Refresh
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
# Transportable Detonation Chamber
|
# Transportable Detonation Chamber
|
||||||
|
|
||||||
A pre-configured Windows 11 VM for malware detonation testing against multiple EDR solutions.
|
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
|
## 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 │ │
|
│ │ Detonator │───▶│ DetonatorAgent │ │ LitterBox │ │
|
||||||
@@ -40,28 +41,114 @@ Uses Vagrant + Hyper-V to provision a fully automated analysis environment.
|
|||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ ▼ ▼ │
|
│ ▼ ▼ │
|
||||||
│ Windows Event Log NDJSON alerts │
|
│ 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
|
## 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))
|
- **Vagrant** >= 2.4 ([download](https://www.vagrantup.com/downloads))
|
||||||
- **Administrator** PowerShell (required for Hyper-V)
|
- **Administrator** PowerShell (required for Hyper-V)
|
||||||
- ~30 GB free disk space
|
- ~30 GB free disk space
|
||||||
- ~8 GB RAM available for the VM
|
- ~8 GB RAM available for the VM
|
||||||
|
|
||||||
### Enable Hyper-V
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Run as Administrator
|
# Enable Hyper-V (run as Administrator, reboot required)
|
||||||
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All
|
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
|
## Quick Start
|
||||||
|
|
||||||
|
### Windows (Hyper-V)
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Clone this repo
|
# Clone this repo
|
||||||
git clone https://github.com/your-user/transportable-detonation-chamber.git
|
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)
|
# 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:
|
Once provisioning completes, the services start automatically:
|
||||||
|
|
||||||
- **Unified Web UI**: http://localhost:9000 (recommended - integrates everything)
|
- **Unified Web UI**: http://localhost:9000 (recommended - integrates everything)
|
||||||
@@ -136,6 +243,8 @@ Invoke-RestMethod -Uri "http://localhost:8080/api/lock/status"
|
|||||||
|
|
||||||
## VM Management
|
## VM Management
|
||||||
|
|
||||||
|
### Common Commands (both platforms)
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Stop the VM
|
# Stop the VM
|
||||||
vagrant halt
|
vagrant halt
|
||||||
@@ -154,7 +263,8 @@ vagrant provision
|
|||||||
|
|
||||||
# Destroy and rebuild from scratch
|
# Destroy and rebuild from scratch
|
||||||
vagrant destroy -f
|
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)
|
# Take a snapshot (recommended before detonation)
|
||||||
vagrant snapshot save clean_state
|
vagrant snapshot save clean_state
|
||||||
@@ -163,6 +273,23 @@ vagrant snapshot save clean_state
|
|||||||
vagrant snapshot restore 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
|
## Configuration
|
||||||
|
|
||||||
### Custom Fibratus rules
|
### Custom Fibratus rules
|
||||||
@@ -204,22 +331,25 @@ Set-MpPreference -DisableRealtimeMonitoring $true
|
|||||||
|
|
||||||
```
|
```
|
||||||
transportable-detonation-chamber/
|
transportable-detonation-chamber/
|
||||||
├── Vagrantfile # VM definition
|
├── Vagrantfile # VM definition (Windows host, Hyper-V)
|
||||||
|
├── Vagrantfile.utm # VM definition (macOS Apple Silicon, QEMU)
|
||||||
├── config/
|
├── config/
|
||||||
│ ├── fibratus.yml # Fibratus EDR config (JSON eventlog output)
|
│ ├── 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
|
│ └── profiles_init.yaml # Detonator target profiles
|
||||||
├── webui/ # Unified web interface
|
├── webui/ # Unified web interface
|
||||||
│ ├── app.py # Flask backend (API aggregation)
|
│ ├── app.py # Flask backend (API aggregation, alert loading)
|
||||||
│ ├── requirements.txt
|
│ ├── requirements.txt
|
||||||
│ ├── templates/index.html # SPA shell
|
│ ├── templates/index.html # SPA shell
|
||||||
│ └── static/
|
│ └── static/
|
||||||
│ ├── css/style.css # Dark theme (Rustinel-inspired)
|
│ ├── css/style.css # Dark theme (Rustinel-inspired)
|
||||||
│ └── js/app.js # Frontend logic
|
│ └── js/app.js # Frontend logic (process tree, detail panels)
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ ├── install-prerequisites.ps1 # .NET 8, Python, Git, Chocolatey
|
│ ├── install-prerequisites.ps1 # .NET 8, Python 3.12, Git, Chocolatey, 7-Zip
|
||||||
│ ├── install-fibratus.ps1 # Fibratus v3.0.0
|
│ ├── install-sysmon.ps1 # Sysmon (ARM64-aware: Sysmon64a.exe)
|
||||||
│ ├── install-rustinel.ps1 # Rustinel v1.1.1
|
│ ├── 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-detonator.ps1 # Detonator + DetonatorAgent from source
|
||||||
│ ├── install-litterbox.ps1 # LitterBox payload analysis sandbox
|
│ ├── install-litterbox.ps1 # LitterBox payload analysis sandbox
|
||||||
│ ├── install-webui.ps1 # Unified web UI
|
│ ├── 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
|
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
|
## Security Notes
|
||||||
|
|
||||||
- This VM is designed for **malware analysis** - treat it as compromised
|
- 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
|
- Defender exclusions are configured for detonation paths only
|
||||||
- Rustinel active response is **disabled by default** - enable after testing
|
- 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
|
## Credits
|
||||||
|
|
||||||
- [dobin/detonator](https://github.com/dobin/detonator) - Orchestration framework
|
- [dobin/detonator](https://github.com/dobin/detonator) - Orchestration framework
|
||||||
|
|||||||
+143
@@ -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
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
@@ -84,6 +84,7 @@ Add-MpPreference -ExclusionPath $infectedDir -ErrorAction SilentlyContinue
|
|||||||
Add-MpPreference -ExclusionPath "C:\samples" -ErrorAction SilentlyContinue
|
Add-MpPreference -ExclusionPath "C:\samples" -ErrorAction SilentlyContinue
|
||||||
Add-MpPreference -ExclusionPath "C:\Users\Public\Downloads" -ErrorAction SilentlyContinue
|
Add-MpPreference -ExclusionPath "C:\Users\Public\Downloads" -ErrorAction SilentlyContinue
|
||||||
Add-MpPreference -ExclusionPath "C:\LitterBox" -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 "[+] Sample directories created and excluded from Defender" -ForegroundColor Green
|
||||||
Write-Host " Desktop\infected: $infectedDir" -ForegroundColor Gray
|
Write-Host " Desktop\infected: $infectedDir" -ForegroundColor Gray
|
||||||
Write-Host " Samples: C:\samples" -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
|
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
|
Unregister-ScheduledTask -TaskName "Rustinel" -Confirm:$false -ErrorAction SilentlyContinue
|
||||||
$ps1Content = @"
|
$action = New-ScheduledTaskAction -Execute $rustinelExe -Argument "run" -WorkingDirectory $rustinelDir
|
||||||
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
|
|
||||||
$trigger = New-ScheduledTaskTrigger -AtStartup
|
$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
|
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
|
||||||
Register-ScheduledTask -TaskName "Rustinel" -Action $action -Trigger $trigger -Settings $settings -Principal $principal | Out-Null
|
Register-ScheduledTask -TaskName "Rustinel" -Action $action -Trigger $trigger -Settings $settings -Principal $principal | Out-Null
|
||||||
Start-ScheduledTask -TaskName "Rustinel"
|
Start-ScheduledTask -TaskName "Rustinel"
|
||||||
Start-Sleep -Seconds 3
|
Start-Sleep -Seconds 5
|
||||||
Write-Host "[+] Rustinel registered and started" -ForegroundColor Green
|
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 {
|
} else {
|
||||||
Write-Host "[!] Rustinel not found at $rustinelExe - skipping" -ForegroundColor Yellow
|
Write-Host "[!] Rustinel not found at $rustinelExe - skipping" -ForegroundColor Yellow
|
||||||
}
|
}
|
||||||
@@ -161,8 +168,19 @@ $agentExe = "$detonatorAgentDir\publish\DetonatorAgent.exe"
|
|||||||
$agentDll = "$detonatorAgentDir\publish\DetonatorAgent.dll"
|
$agentDll = "$detonatorAgentDir\publish\DetonatorAgent.dll"
|
||||||
|
|
||||||
if (Test-Path $agentExe) {
|
if (Test-Path $agentExe) {
|
||||||
Register-ServiceTask -Name "DetonatorAgent" -Command $agentExe -Arguments "--port 8080 --edr fibratus" -WorkingDirectory "$detonatorAgentDir\publish"
|
# DetonatorAgent runs directly (no CMD wrapper needed for .NET apps)
|
||||||
Start-Sleep -Seconds 3
|
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) {
|
} elseif (Test-Path $agentDll) {
|
||||||
$dotnetExe = (Get-Command dotnet -ErrorAction SilentlyContinue).Source
|
$dotnetExe = (Get-Command dotnet -ErrorAction SilentlyContinue).Source
|
||||||
if ($dotnetExe) {
|
if ($dotnetExe) {
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ Set-StrictMode -Version Latest
|
|||||||
|
|
||||||
Write-Host "=== Installing Fibratus ===" -ForegroundColor Cyan
|
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"
|
$fibratusVersion = "3.0.0"
|
||||||
$fibratusInstallDir = "$env:ProgramFiles\Fibratus"
|
$fibratusInstallDir = "$env:ProgramFiles\Fibratus"
|
||||||
$fibratusMsiUrl = "https://github.com/rabbitstack/fibratus/releases/download/v${fibratusVersion}/fibratus-${fibratusVersion}-amd64.msi"
|
$fibratusMsiUrl = "https://github.com/rabbitstack/fibratus/releases/download/v${fibratusVersion}/fibratus-${fibratusVersion}-amd64.msi"
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ Set-StrictMode -Version Latest
|
|||||||
|
|
||||||
Write-Host "=== Installing Rustinel ===" -ForegroundColor Cyan
|
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"
|
$rustinelVersion = "1.1.1"
|
||||||
$rustinelInstallDir = "C:\tools\rustinel"
|
$rustinelInstallDir = "C:\tools\rustinel"
|
||||||
$rustinelZipUrl = "https://github.com/Karib0u/rustinel/releases/download/v${rustinelVersion}/rustinel-${rustinelVersion}-x86_64-pc-windows-msvc.zip"
|
$rustinelZipUrl = "https://github.com/Karib0u/rustinel/releases/download/v${rustinelVersion}/rustinel-${rustinelVersion}-x86_64-pc-windows-msvc.zip"
|
||||||
|
|||||||
+38
-11
@@ -22,12 +22,25 @@ Set-StrictMode -Version Latest
|
|||||||
|
|
||||||
Write-Host "=== Installing Sysmon ===" -ForegroundColor Cyan
|
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"
|
$sysmonDir = "C:\tools\sysmon"
|
||||||
$sysmonExe = "$sysmonDir\Sysmon64.exe"
|
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"
|
$configPath = "$sysmonDir\sysmonconfig.xml"
|
||||||
|
|
||||||
# Check if already installed and running
|
# Check if already installed and running
|
||||||
$sysmonSvc = Get-Service -Name "Sysmon64" -ErrorAction SilentlyContinue
|
$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") {
|
if ($sysmonSvc -and $sysmonSvc.Status -eq "Running") {
|
||||||
Write-Host "[+] Sysmon already installed and running" -ForegroundColor Green
|
Write-Host "[+] Sysmon already installed and running" -ForegroundColor Green
|
||||||
Write-Host "[*] Updating configuration..." -ForegroundColor Yellow
|
Write-Host "[*] Updating configuration..." -ForegroundColor Yellow
|
||||||
@@ -72,11 +85,21 @@ Expand-Archive -Path $sysmonZipPath -DestinationPath $sysmonDir -Force
|
|||||||
Remove-Item $sysmonZipPath -Force -ErrorAction SilentlyContinue
|
Remove-Item $sysmonZipPath -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
if (-not (Test-Path $sysmonExe)) {
|
if (-not (Test-Path $sysmonExe)) {
|
||||||
Write-Host "[!] Sysmon64.exe not found after extraction" -ForegroundColor Red
|
Write-Host "[!] $([System.IO.Path]::GetFileName($sysmonExe)) not found after extraction" -ForegroundColor Red
|
||||||
# Check for alternate name
|
# Check for alternate names
|
||||||
if (Test-Path "$sysmonDir\sysmon64.exe") {
|
$candidates = @("$sysmonDir\Sysmon64a.exe", "$sysmonDir\Sysmon64.exe", "$sysmonDir\sysmon64.exe", "$sysmonDir\sysmon64a.exe")
|
||||||
$sysmonExe = "$sysmonDir\sysmon64.exe"
|
$found = $false
|
||||||
} else {
|
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
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,14 +149,18 @@ Write-Host "[*] Installing Sysmon service..." -ForegroundColor Yellow
|
|||||||
|
|
||||||
# Verify installation
|
# Verify installation
|
||||||
Start-Sleep -Seconds 3
|
Start-Sleep -Seconds 3
|
||||||
$sysmonSvc = Get-Service -Name "Sysmon64" -ErrorAction SilentlyContinue
|
$sysmonSvc = Get-Service -Name $sysmonServiceName -ErrorAction SilentlyContinue
|
||||||
|
if (-not $sysmonSvc) {
|
||||||
|
$sysmonSvc = Get-Service -Name "Sysmon64" -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
if ($sysmonSvc -and $sysmonSvc.Status -eq "Running") {
|
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 {
|
} else {
|
||||||
# Try starting it
|
# 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
|
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") {
|
if ($sysmonSvc -and $sysmonSvc.Status -eq "Running") {
|
||||||
Write-Host "[+] Sysmon installed and started" -ForegroundColor Green
|
Write-Host "[+] Sysmon installed and started" -ForegroundColor Green
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+485
-17
@@ -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")
|
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")
|
LITTERBOX_API = os.environ.get("LITTERBOX_API", "http://127.0.0.1:1337")
|
||||||
WEBUI_PORT = int(os.environ.get("WEBUI_PORT", "9000"))
|
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)
|
# In-memory event store (populated from Rustinel NDJSON + Fibratus)
|
||||||
events_store = {
|
events_store = {
|
||||||
@@ -42,6 +43,69 @@ events_store = {
|
|||||||
}
|
}
|
||||||
store_lock = threading.Lock()
|
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 ---
|
# --- Rustinel NDJSON Parser ---
|
||||||
def _get_nested(data, dotted_key, default=None):
|
def _get_nested(data, dotted_key, default=None):
|
||||||
@@ -141,8 +205,241 @@ def load_rustinel_alerts():
|
|||||||
return unique_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):
|
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 = {}
|
processes = {}
|
||||||
for alert in alerts:
|
for alert in alerts:
|
||||||
pid = alert.get("pid")
|
pid = alert.get("pid")
|
||||||
@@ -166,24 +463,48 @@ def build_process_tree(alerts):
|
|||||||
"file": 0, "network": 0, "dns": 0, "http": 0,
|
"file": 0, "network": 0, "dns": 0, "http": 0,
|
||||||
"registry": 0, "modules": 0, "scripts": 0, "injection": 0,
|
"registry": 0, "modules": 0, "scripts": 0, "injection": 0,
|
||||||
"wmi": 0, "services": 0, "tasks": 0, "logons": 0,
|
"wmi": 0, "services": 0, "tasks": 0, "logons": 0,
|
||||||
"artifacts": 0, "threats": 0,
|
"artifacts": 0, "threats": 0, "detonated": 0,
|
||||||
},
|
},
|
||||||
"alerts": [],
|
"alerts": [],
|
||||||
|
"detonated": False,
|
||||||
|
"detonation_sources": [],
|
||||||
}
|
}
|
||||||
if pid:
|
if pid:
|
||||||
# Count activity by category
|
# Handle PID reuse: if the executable changed, update process identity
|
||||||
cat = alert.get("category", "")
|
# (later alerts overwrite older ones so the most recent process info wins)
|
||||||
if isinstance(cat, list):
|
|
||||||
cat = cat[0] if cat else ""
|
|
||||||
cat_lower = cat.lower()
|
|
||||||
proc_entry = processes.get(pid)
|
proc_entry = processes.get(pid)
|
||||||
if proc_entry:
|
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)
|
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["activity"]["threats"] += 1
|
proc_entry["activity"]["threats"] += 1
|
||||||
if "file" in cat_lower:
|
if "file" in cat_lower:
|
||||||
proc_entry["activity"]["file"] += 1
|
proc_entry["activity"]["file"] += 1
|
||||||
elif "network" in cat_lower:
|
elif "network" in cat_lower:
|
||||||
proc_entry["activity"]["network"] += 1
|
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:
|
elif "dns" in cat_lower:
|
||||||
proc_entry["activity"]["dns"] += 1
|
proc_entry["activity"]["dns"] += 1
|
||||||
elif "registry" in cat_lower:
|
elif "registry" in cat_lower:
|
||||||
@@ -191,6 +512,19 @@ def build_process_tree(alerts):
|
|||||||
elif "process" in cat_lower:
|
elif "process" in cat_lower:
|
||||||
proc_entry["activity"]["modules"] += 1
|
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
|
# Track last seen timestamp
|
||||||
ts = alert.get("timestamp", "")
|
ts = alert.get("timestamp", "")
|
||||||
if ts and ts > (proc_entry.get("last_seen") or ""):
|
if ts and ts > (proc_entry.get("last_seen") or ""):
|
||||||
@@ -232,13 +566,32 @@ def build_process_tree(alerts):
|
|||||||
|
|
||||||
# --- Background alert loader ---
|
# --- Background alert loader ---
|
||||||
def alert_loader_thread():
|
def alert_loader_thread():
|
||||||
"""Periodically reload alerts from Rustinel."""
|
"""Periodically reload alerts from Rustinel, Fibratus, and LitterBox."""
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
alerts = load_rustinel_alerts()
|
# Load from all sources
|
||||||
processes = build_process_tree(alerts)
|
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:
|
with store_lock:
|
||||||
events_store["alerts"] = alerts
|
events_store["alerts"] = unique_alerts
|
||||||
events_store["processes"] = processes
|
events_store["processes"] = processes
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[alert_loader] Error: {e}")
|
print(f"[alert_loader] Error: {e}")
|
||||||
@@ -462,16 +815,19 @@ def api_alerts():
|
|||||||
"""Get all Rustinel/Fibratus alerts."""
|
"""Get all Rustinel/Fibratus alerts."""
|
||||||
with store_lock:
|
with store_lock:
|
||||||
alerts = events_store.get("alerts", [])
|
alerts = events_store.get("alerts", [])
|
||||||
# Filter by severity/engine if requested
|
# Filter by severity/engine/detonated if requested
|
||||||
severity = request.args.get("severity")
|
severity = request.args.get("severity")
|
||||||
engine = request.args.get("engine")
|
engine = request.args.get("engine")
|
||||||
pid = request.args.get("pid", type=int)
|
pid = request.args.get("pid", type=int)
|
||||||
since = request.args.get("since") # ISO timestamp - only alerts after this time
|
since = request.args.get("since") # ISO timestamp - only alerts after this time
|
||||||
|
detonated = request.args.get("detonated")
|
||||||
|
|
||||||
if severity:
|
if severity:
|
||||||
alerts = [a for a in alerts if a.get("severity", "").lower() == severity.lower()]
|
alerts = [a for a in alerts if a.get("severity", "").lower() == severity.lower()]
|
||||||
if engine:
|
if engine:
|
||||||
alerts = [a for a in alerts if a.get("engine", "").lower() == engine.lower()]
|
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:
|
if pid:
|
||||||
alerts = [a for a in alerts if a.get("pid") == pid]
|
alerts = [a for a in alerts if a.get("pid") == pid]
|
||||||
if since:
|
if since:
|
||||||
@@ -799,9 +1155,20 @@ def api_submit():
|
|||||||
"sha256": file_sha256,
|
"sha256": file_sha256,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Record submission in history
|
||||||
|
_record_submission(filename, file_sha256, len(file_bytes), target, results)
|
||||||
|
|
||||||
return jsonify(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=""):
|
def _add_hash_to_ioc(sha256_hash, filename=""):
|
||||||
"""Add a file hash to Rustinel's IOC hash feed for real-time detection."""
|
"""Add a file hash to Rustinel's IOC hash feed for real-time detection."""
|
||||||
# Try multiple possible IOC paths
|
# Try multiple possible IOC paths
|
||||||
@@ -867,9 +1234,10 @@ def api_file_download():
|
|||||||
|
|
||||||
@app.route("/api/file/hex")
|
@app.route("/api/file/hex")
|
||||||
def 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", "")
|
filepath = request.args.get("path", "")
|
||||||
num_bytes = min(request.args.get("bytes", 8192, type=int), 65536)
|
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:
|
if not filepath:
|
||||||
return jsonify({"error": "No path specified"}), 400
|
return jsonify({"error": "No path specified"}), 400
|
||||||
@@ -879,27 +1247,127 @@ def api_file_hex():
|
|||||||
return jsonify({"error": "File not found", "hex": ""}), 404
|
return jsonify({"error": "File not found", "hex": ""}), 404
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
file_size = os.path.getsize(norm_path)
|
||||||
with open(norm_path, "rb") as f:
|
with open(norm_path, "rb") as f:
|
||||||
|
f.seek(start_offset)
|
||||||
data = f.read(num_bytes)
|
data = f.read(num_bytes)
|
||||||
|
|
||||||
# Generate hex dump
|
# Generate hex dump
|
||||||
lines = []
|
lines = []
|
||||||
for offset in range(0, len(data), 16):
|
for line_offset in range(0, len(data), 16):
|
||||||
chunk = data[offset:offset + 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])
|
||||||
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)
|
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({
|
return jsonify({
|
||||||
"hex": "\n".join(lines),
|
"hex": "\n".join(lines),
|
||||||
"size": os.path.getsize(norm_path),
|
"size": file_size,
|
||||||
"bytes_shown": len(data),
|
"bytes_shown": len(data),
|
||||||
|
"offset": start_offset,
|
||||||
|
"raw_bytes": raw_bytes,
|
||||||
})
|
})
|
||||||
except (IOError, OSError) as e:
|
except (IOError, OSError) as e:
|
||||||
return jsonify({"error": str(e), "hex": ""}), 500
|
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 ---
|
# --- Main ---
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Start background alert loader
|
# Start background alert loader
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
+2455
-1549
File diff suppressed because it is too large
Load Diff
+245
-20
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Detonation Chamber</title>
|
<title>Detonation Chamber</title>
|
||||||
<link rel="stylesheet" href="/static/css/style.css">
|
<link rel="stylesheet" href="/static/css/style.css?v=2">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
@@ -15,7 +15,9 @@
|
|||||||
<nav class="sidebar-tabs">
|
<nav class="sidebar-tabs">
|
||||||
<button class="tab active" data-tab="dashboard">Dashboard</button>
|
<button class="tab active" data-tab="dashboard">Dashboard</button>
|
||||||
<button class="tab" data-tab="tracing">Tracing</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="sysmon">Sysmon</button>
|
||||||
|
<button class="tab" data-tab="hexeditor">Hex</button>
|
||||||
<button class="tab" data-tab="submit">Submit</button>
|
<button class="tab" data-tab="submit">Submit</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,28 +65,173 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tracing Tab -->
|
<!-- Tracing Tab - RUSTINEL TRACE Analysis Console -->
|
||||||
<div class="tab-content" id="tab-tracing">
|
<div class="tab-content" id="tab-tracing">
|
||||||
<div class="content-header">
|
<!-- Console Header Bar -->
|
||||||
<h2>Event Tracing</h2>
|
<div class="rtrace-header">
|
||||||
<div class="header-actions">
|
<div class="rtrace-title">
|
||||||
<select id="filter-severity" class="filter-select">
|
<span class="rtrace-brand"><span class="rtrace-brand-highlight">RUSTINEL TRACE</span> Analysis Console</span>
|
||||||
<option value="">All Severities</option>
|
</div>
|
||||||
<option value="critical">Critical</option>
|
<div class="rtrace-header-actions">
|
||||||
<option value="high">High</option>
|
<select id="rtrace-process-select" class="rtrace-process-dropdown">
|
||||||
<option value="medium">Medium</option>
|
<option value="">-- select process --</option>
|
||||||
<option value="low">Low</option>
|
|
||||||
</select>
|
</select>
|
||||||
<select id="filter-engine" class="filter-select">
|
<button class="btn btn-sm" onclick="refreshAlerts()">↻ Refresh</button>
|
||||||
<option value="">All Engines</option>
|
<button class="btn btn-sm" onclick="clearStoppedProcesses()">⏸ Clear stopped</button>
|
||||||
<option value="sigma">Sigma</option>
|
<button class="btn btn-sm" onclick="clearAllTracing()">⏹ Clear all</button>
|
||||||
<option value="yara">YARA</option>
|
|
||||||
<option value="ioc">IOC</option>
|
|
||||||
</select>
|
|
||||||
<button class="btn btn-sm" onclick="refreshAlerts()">Refresh</button>
|
|
||||||
</div>
|
</div>
|
||||||
</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>) — 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">🔍</span>
|
||||||
|
<span class="graph-search-clear" id="graph-search-clear">×</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>
|
</div>
|
||||||
|
|
||||||
<!-- Sysmon Tab -->
|
<!-- Sysmon Tab -->
|
||||||
@@ -117,6 +264,74 @@
|
|||||||
<div id="sysmon-table" class="events-table"></div>
|
<div id="sysmon-table" class="events-table"></div>
|
||||||
</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()">◀ Prev</button>
|
||||||
|
<button class="btn btn-sm" onclick="hexNextPage()">Next ▶</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Drop zone + path input -->
|
||||||
|
<div class="hex-drop-zone" id="hex-drop-zone">
|
||||||
|
<div class="hex-drop-icon">📂</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 -->
|
<!-- Submit Tab -->
|
||||||
<div class="tab-content" id="tab-submit">
|
<div class="tab-content" id="tab-submit">
|
||||||
<div class="content-header">
|
<div class="content-header">
|
||||||
@@ -148,6 +363,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="submit-result" class="submit-result"></div>
|
<div id="submit-result" class="submit-result"></div>
|
||||||
</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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -168,6 +393,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/app.js"></script>
|
<script src="/static/js/app.js?v=2"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user