Reworked a little and some screenshots
@@ -108,6 +108,16 @@ Vagrant.configure("2") do |config|
|
||||
path: "scripts/install-hunt-sleeping-beacons.ps1",
|
||||
privileged: true
|
||||
|
||||
config.vm.provision "beaconeye",
|
||||
type: "shell",
|
||||
path: "scripts/install-beaconeye.ps1",
|
||||
privileged: true
|
||||
|
||||
config.vm.provision "scanner-tools",
|
||||
type: "shell",
|
||||
path: "scripts/install-scanner-tools.ps1",
|
||||
privileged: true
|
||||
|
||||
config.vm.provision "re-tools",
|
||||
type: "shell",
|
||||
path: "scripts/install-re-tools.ps1",
|
||||
|
||||
@@ -206,6 +206,16 @@ Vagrant.configure("2") do |config|
|
||||
path: "scripts/install-hunt-sleeping-beacons.ps1",
|
||||
privileged: true
|
||||
|
||||
config.vm.provision "beaconeye",
|
||||
type: "shell",
|
||||
path: "scripts/install-beaconeye.ps1",
|
||||
privileged: true
|
||||
|
||||
config.vm.provision "scanner-tools",
|
||||
type: "shell",
|
||||
path: "scripts/install-scanner-tools.ps1",
|
||||
privileged: true
|
||||
|
||||
config.vm.provision "re-tools",
|
||||
type: "shell",
|
||||
path: "scripts/install-re-tools.ps1",
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Transportable Detonation Chamber - Frontend Development Mode
|
||||
|
||||
.DESCRIPTION
|
||||
Starts the Web UI in development mode with:
|
||||
- Live-reload for CSS/JS/HTML changes (auto-refreshes browser)
|
||||
- Flask debug mode (auto-restarts on Python changes)
|
||||
- Auto-opens browser on startup
|
||||
- Optional mock mode for offline development
|
||||
|
||||
.EXAMPLE
|
||||
.\dev.ps1 Start dev server (default: port 9000)
|
||||
.\dev.ps1 -Mock Start with mock backend services
|
||||
.\dev.ps1 -Port 8080 Start on a custom port
|
||||
.\dev.ps1 -NoOpen Don't auto-open the browser
|
||||
#>
|
||||
|
||||
param(
|
||||
[Parameter()]
|
||||
[int]$Port = 9000,
|
||||
|
||||
[Parameter()]
|
||||
[switch]$Mock,
|
||||
|
||||
[Parameter()]
|
||||
[switch]$NoOpen,
|
||||
|
||||
[Parameter()]
|
||||
[string]$Host = "127.0.0.1"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$VenvDir = "webui\.venv"
|
||||
$PyExe = "$VenvDir\Scripts\python.exe"
|
||||
|
||||
# --- Preflight Checks ---
|
||||
|
||||
# Check venv exists
|
||||
if (-not (Test-Path $PyExe)) {
|
||||
Write-Host ""
|
||||
Write-Host " [ERROR] Python venv not found." -ForegroundColor Red
|
||||
Write-Host " Run '.\make.ps1 install' first to set up the environment." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Verify watchdog is installed (needed for live-reload)
|
||||
$watchdogCheck = & $PyExe -c "import watchdog; print('ok')" 2>$null
|
||||
if ($watchdogCheck -ne 'ok') {
|
||||
Write-Host "[dev] Installing missing dependency: watchdog..." -ForegroundColor Yellow
|
||||
& $PyExe -m pip install watchdog -q
|
||||
}
|
||||
|
||||
# --- Banner ---
|
||||
Write-Host ""
|
||||
Write-Host " ================================================================" -ForegroundColor DarkCyan
|
||||
Write-Host " Transportable Detonation Chamber" -ForegroundColor Cyan -NoNewline
|
||||
Write-Host " - Dev Mode" -ForegroundColor Yellow
|
||||
Write-Host " ================================================================" -ForegroundColor DarkCyan
|
||||
Write-Host ""
|
||||
Write-Host " Features:" -ForegroundColor White
|
||||
Write-Host " * Live-reload CSS/JS/HTML changes refresh browser automatically" -ForegroundColor DarkGray
|
||||
Write-Host " * Debug mode Python changes restart the server" -ForegroundColor DarkGray
|
||||
Write-Host " * File watcher Console shows file change events" -ForegroundColor DarkGray
|
||||
if ($Mock) {
|
||||
Write-Host " * Mock mode Backend services are stubbed" -ForegroundColor DarkGray
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
# --- Build command args ---
|
||||
$devArgs = @("webui\dev_server.py", "--port", $Port, "--host", $Host)
|
||||
|
||||
if ($Mock) {
|
||||
$devArgs += "--mock"
|
||||
}
|
||||
|
||||
if ($NoOpen) {
|
||||
$devArgs += "--no-open"
|
||||
}
|
||||
|
||||
# --- Start Dev Server ---
|
||||
Write-Host " Starting dev server..." -ForegroundColor Cyan
|
||||
Write-Host " URL: http://${Host}:${Port}" -ForegroundColor Green
|
||||
Write-Host " Stop: Ctrl+C" -ForegroundColor DarkGray
|
||||
Write-Host ""
|
||||
|
||||
try {
|
||||
& $PyExe $devArgs
|
||||
} catch {
|
||||
# Ctrl+C is expected
|
||||
if ($_.Exception.Message -notmatch 'PipelineStoppedException') {
|
||||
Write-Host ""
|
||||
Write-Host " [ERROR] Dev server exited with error:" -ForegroundColor Red
|
||||
Write-Host " $($_.Exception.Message)" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
}
|
||||
} finally {
|
||||
Write-Host ""
|
||||
Write-Host " Dev server stopped." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
param(
|
||||
[Parameter(Position=0)]
|
||||
[ValidateSet(
|
||||
'help','prerequisites','install','run','run-debug','uninstall',
|
||||
'help','prerequisites','install','run','run-debug','dev','uninstall',
|
||||
'up','halt','destroy','reload','provision','provision-webui',
|
||||
'deploy','deploy-app','restart','deploy-restart','open','logs',
|
||||
'ssh','rdp','status','services','alerts','test','submit',
|
||||
@@ -79,6 +79,7 @@ switch ($Target) {
|
||||
Write-Host " .\make.ps1 install Install Python venv + dependencies"
|
||||
Write-Host " .\make.ps1 run Run the Web UI locally (port 9000)"
|
||||
Write-Host " .\make.ps1 run-debug Run with auto-reload on file changes"
|
||||
Write-Host " .\make.ps1 dev Dev mode: live-reload + file watcher"
|
||||
Write-Host " .\make.ps1 uninstall Remove local venv"
|
||||
Write-Host ""
|
||||
Write-Host " VM Lifecycle:" -ForegroundColor Yellow
|
||||
@@ -248,6 +249,11 @@ switch ($Target) {
|
||||
}
|
||||
}
|
||||
|
||||
'dev' {
|
||||
Write-Host "[dev] Starting frontend development mode..." -ForegroundColor Cyan
|
||||
& "$PSScriptRoot\dev.ps1" -Port $( if ($env:WEBUI_PORT) { $env:WEBUI_PORT } else { 9000 } )
|
||||
}
|
||||
|
||||
'uninstall' {
|
||||
$VenvDir = "webui\.venv"
|
||||
if (Test-Path $VenvDir) {
|
||||
|
||||
@@ -0,0 +1,740 @@
|
||||
# =============================================================================
|
||||
# Incident Response Playbook: Malware Found on Endpoint
|
||||
# =============================================================================
|
||||
# Framework: NIST SP 800-61r2 / SANS Incident Response
|
||||
# Version: 1.0
|
||||
# Author: DetonationChamber
|
||||
# Created: 2026-06-13
|
||||
# Severity: High - Critical (context-dependent)
|
||||
# =============================================================================
|
||||
|
||||
id: PB-IR-001
|
||||
title: "Malware Found on Endpoint"
|
||||
version: "1.0"
|
||||
status: active
|
||||
author: DetonationChamber
|
||||
created: 2026-06-13
|
||||
last_updated: 2026-06-13
|
||||
|
||||
classification:
|
||||
type: incident_response
|
||||
category: malware
|
||||
severity_default: high
|
||||
escalation_threshold: critical
|
||||
|
||||
# Maps to detection rules that trigger this playbook
|
||||
triggers:
|
||||
sigma_rules:
|
||||
- process_injection.yml
|
||||
- credential_access_lsass.yml
|
||||
- suspicious_powershell.yml
|
||||
- persistence_schtask.yml
|
||||
- persistence_startup.yml
|
||||
- persistence_registry.yml
|
||||
- lolbins_execution.yml
|
||||
- suspicious_process_creation.yml
|
||||
yara_rules:
|
||||
- SuspiciousPEImports
|
||||
- SuspiciousPEStrings
|
||||
- PackedOrEncryptedPE
|
||||
engines:
|
||||
- sigma
|
||||
- yara
|
||||
- fibratus
|
||||
- litterbox
|
||||
alert_severities:
|
||||
- high
|
||||
- critical
|
||||
|
||||
mitre_attack:
|
||||
tactics:
|
||||
- TA0001 # Initial Access
|
||||
- TA0002 # Execution
|
||||
- TA0003 # Persistence
|
||||
- TA0004 # Privilege Escalation
|
||||
- TA0005 # Defense Evasion
|
||||
- TA0006 # Credential Access
|
||||
- TA0007 # Discovery
|
||||
- TA0008 # Lateral Movement
|
||||
- TA0009 # Collection
|
||||
- TA0010 # Exfiltration
|
||||
- TA0011 # Command and Control
|
||||
techniques:
|
||||
- T1055 # Process Injection
|
||||
- T1059.001 # PowerShell
|
||||
- T1003.001 # LSASS Memory
|
||||
- T1027 # Obfuscated Files
|
||||
- T1053.005 # Scheduled Task
|
||||
- T1547.001 # Registry Run Keys
|
||||
- T1071 # Application Layer Protocol (C2)
|
||||
- T1486 # Data Encrypted for Impact (Ransomware)
|
||||
|
||||
# =============================================================================
|
||||
# PHASE 1: DETECTION & INITIAL TRIAGE
|
||||
# =============================================================================
|
||||
|
||||
phase_1_detection:
|
||||
title: "Detection & Initial Triage"
|
||||
objective: "Confirm the alert, determine scope, and assess severity"
|
||||
max_time: "30 minutes"
|
||||
|
||||
step_1_alert_validation:
|
||||
action: "Validate the alert is a true positive"
|
||||
description: |
|
||||
Determine if the detection represents actual malware or a false positive.
|
||||
Cross-reference multiple detection engines for corroboration.
|
||||
procedures:
|
||||
- description: "Review the triggering alert in Detonation Chamber UI"
|
||||
check: "Navigate to Dashboard > Alerts and examine the detection details"
|
||||
- description: "Check detection engine consensus"
|
||||
check: |
|
||||
Multiple engines detecting the same artifact increases confidence.
|
||||
Single low-confidence Sigma rule hit alone may be FP.
|
||||
YARA match + behavioral detection = high confidence TP.
|
||||
- description: "Verify file hash against threat intelligence"
|
||||
commands:
|
||||
windows: |
|
||||
Get-FileHash -Algorithm SHA256 -Path "<SUSPECT_FILE>"
|
||||
# Query hash against:
|
||||
# - VirusTotal (API or manual)
|
||||
# - MalwareBazaar
|
||||
# - Internal threat intel platform
|
||||
- description: "Check if file is signed and verify publisher"
|
||||
commands:
|
||||
windows: |
|
||||
Get-AuthenticodeSignature -FilePath "<SUSPECT_FILE>"
|
||||
# Unsigned binary in system directories = suspicious
|
||||
# Signature from unknown publisher = suspicious
|
||||
# Valid Microsoft/known vendor signature = likely FP
|
||||
- description: "Review process behavior in Tracing tab"
|
||||
check: |
|
||||
Look for:
|
||||
- Child process spawning (especially cmd.exe, powershell.exe)
|
||||
- Network connections to external IPs
|
||||
- File writes to temp/startup directories
|
||||
- Registry modifications to Run keys
|
||||
decision:
|
||||
true_positive: "Proceed to Step 2 (Severity Assessment)"
|
||||
false_positive: "Document FP, tune detection rule, close alert"
|
||||
uncertain: "Detonate sample in sandbox (Submit tab), await results"
|
||||
|
||||
step_2_severity_assessment:
|
||||
action: "Classify the malware type and determine severity"
|
||||
description: |
|
||||
Based on observed behaviors and indicators, classify the threat
|
||||
and assign operational severity.
|
||||
malware_classification:
|
||||
ransomware:
|
||||
indicators:
|
||||
- "Mass file encryption (.encrypted, .locked, .crypt extensions)"
|
||||
- "Ransom note creation (README.txt, DECRYPT.html)"
|
||||
- "Shadow copy deletion (vssadmin delete shadows)"
|
||||
- "Disabling recovery (bcdedit /set recoveryenabled No)"
|
||||
severity: critical
|
||||
escalation: immediate
|
||||
time_sensitivity: "MINUTES - stop encryption spread"
|
||||
wiper:
|
||||
indicators:
|
||||
- "MBR/VBR overwrite"
|
||||
- "Mass file deletion or zeroing"
|
||||
- "Disk enumeration + destructive writes"
|
||||
severity: critical
|
||||
escalation: immediate
|
||||
time_sensitivity: "MINUTES - prevent data destruction"
|
||||
rat_backdoor:
|
||||
indicators:
|
||||
- "Persistent C2 beaconing (regular intervals)"
|
||||
- "Reverse shell / remote desktop capability"
|
||||
- "Keylogging / screen capture"
|
||||
- "Credential harvesting"
|
||||
severity: high
|
||||
escalation: within_1_hour
|
||||
time_sensitivity: "HOURS - attacker has active access"
|
||||
infostealer:
|
||||
indicators:
|
||||
- "Browser credential store access"
|
||||
- "Crypto wallet file access"
|
||||
- "Clipboard monitoring"
|
||||
- "Data staging and exfiltration"
|
||||
severity: high
|
||||
escalation: within_1_hour
|
||||
time_sensitivity: "HOURS - data may already be exfiltrated"
|
||||
cryptominer:
|
||||
indicators:
|
||||
- "High sustained CPU usage"
|
||||
- "Connections to mining pools (stratum protocol)"
|
||||
- "Process masquerading as system process"
|
||||
severity: medium
|
||||
escalation: within_4_hours
|
||||
time_sensitivity: "HOURS - operational impact but no data loss"
|
||||
dropper_loader:
|
||||
indicators:
|
||||
- "Downloads and executes secondary payload"
|
||||
- "Process hollowing / injection into legitimate process"
|
||||
- "Memory-only payload (fileless)"
|
||||
severity: high
|
||||
escalation: within_1_hour
|
||||
time_sensitivity: "HOURS - determines what final payload is"
|
||||
adware_pup:
|
||||
indicators:
|
||||
- "Browser modification"
|
||||
- "Unwanted toolbars/extensions"
|
||||
- "Ad injection"
|
||||
severity: low
|
||||
escalation: standard_queue
|
||||
time_sensitivity: "DAYS - nuisance, not critical"
|
||||
|
||||
step_3_scope_assessment:
|
||||
action: "Determine the blast radius"
|
||||
procedures:
|
||||
- description: "Identify all affected endpoints"
|
||||
commands:
|
||||
windows: |
|
||||
# Search for IOCs across network (if EDR available)
|
||||
# Check for lateral movement indicators:
|
||||
Get-WinEvent -FilterHashtable @{LogName='Security';ID=4624} |
|
||||
Where-Object { $_.Properties[8].Value -eq 3 } |
|
||||
Select-Object -First 20 TimeCreated, @{N='Source';E={$_.Properties[18].Value}}
|
||||
- description: "Check for persistence mechanisms already deployed"
|
||||
commands:
|
||||
windows: |
|
||||
# Scheduled tasks
|
||||
schtasks /query /fo CSV | Select-String -NotMatch "Microsoft"
|
||||
# Run keys
|
||||
Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
|
||||
Get-ItemProperty "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
|
||||
# Services
|
||||
Get-Service | Where-Object { $_.StartType -eq 'Automatic' -and $_.Status -eq 'Running' } |
|
||||
Where-Object { $_.BinaryPathName -notmatch 'Windows|Microsoft|Program Files' }
|
||||
# Startup folder
|
||||
Get-ChildItem "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup"
|
||||
- description: "Review network connections for C2 or lateral movement"
|
||||
commands:
|
||||
windows: |
|
||||
netstat -ano | findstr ESTABLISHED
|
||||
# Cross-reference PIDs with suspicious processes
|
||||
Get-Process | Where-Object { $_.Id -in @(<SUSPECT_PIDS>) } |
|
||||
Select-Object Id, ProcessName, Path
|
||||
- description: "Check Detonation Chamber UI for correlated alerts"
|
||||
check: |
|
||||
In the Graph tab, examine the process tree for:
|
||||
- Parent-child relationships revealing initial access vector
|
||||
- Sibling processes indicating multi-stage attack
|
||||
- Network connections revealing C2 infrastructure
|
||||
outputs:
|
||||
- "List of affected endpoints (hostnames + IPs)"
|
||||
- "List of affected user accounts"
|
||||
- "C2 infrastructure identified (IPs, domains)"
|
||||
- "Persistence mechanisms cataloged"
|
||||
- "Initial access vector hypothesis"
|
||||
|
||||
# =============================================================================
|
||||
# PHASE 2: CONTAINMENT
|
||||
# =============================================================================
|
||||
|
||||
phase_2_containment:
|
||||
title: "Containment"
|
||||
objective: "Stop the spread and limit damage while preserving evidence"
|
||||
max_time: "2 hours (critical: 15 minutes)"
|
||||
|
||||
principles:
|
||||
- "Preserve forensic evidence before making changes"
|
||||
- "Isolate the endpoint, not destroy the evidence"
|
||||
- "Contain at the network level first, then host level"
|
||||
- "Document every action taken with timestamps"
|
||||
|
||||
step_1_network_isolation:
|
||||
action: "Isolate the affected endpoint from the network"
|
||||
priority: immediate
|
||||
procedures:
|
||||
- description: "Network-level isolation (preferred - preserves host state)"
|
||||
commands:
|
||||
windows: |
|
||||
# Option A: Firewall isolation (allows RDP from IR workstation only)
|
||||
New-NetFirewallRule -DisplayName "IR-Isolate-Block-All" -Direction Outbound -Action Block -Enabled True
|
||||
New-NetFirewallRule -DisplayName "IR-Isolate-Block-Inbound" -Direction Inbound -Action Block -Enabled True
|
||||
# Allow IR workstation access
|
||||
New-NetFirewallRule -DisplayName "IR-Allow-Responder" -Direction Inbound -Action Allow -RemoteAddress "<IR_WORKSTATION_IP>" -Enabled True
|
||||
New-NetFirewallRule -DisplayName "IR-Allow-Responder-Out" -Direction Outbound -Action Allow -RemoteAddress "<IR_WORKSTATION_IP>" -Enabled True
|
||||
network: |
|
||||
# Switch-level port isolation (if available):
|
||||
# - Move VLAN to quarantine network
|
||||
# - Apply ACL to block all traffic except IR tools
|
||||
# EDR-level isolation (if agent supports):
|
||||
# - CrowdFalcon: Network Containment
|
||||
# - Defender ATP: Isolate device
|
||||
# - SentinelOne: Disconnect from network
|
||||
- description: "Block identified C2 infrastructure"
|
||||
commands:
|
||||
firewall: |
|
||||
# Add C2 IPs/domains to network block list:
|
||||
# - Perimeter firewall
|
||||
# - DNS sinkhole
|
||||
# - Proxy/web gateway blocklist
|
||||
# Document all blocked indicators:
|
||||
# IP: x.x.x.x (C2 server)
|
||||
# Domain: malicious-domain.com
|
||||
# Port: 443/tcp
|
||||
warning: |
|
||||
DO NOT simply unplug the network cable or shut down the machine unless:
|
||||
- Active ransomware encryption is in progress
|
||||
- Active data destruction is occurring
|
||||
- Attacker is observed performing live actions
|
||||
In these cases: IMMEDIATE power-off (pull plug, do NOT graceful shutdown)
|
||||
|
||||
step_2_evidence_preservation:
|
||||
action: "Capture volatile evidence before any remediation"
|
||||
priority: high
|
||||
procedures:
|
||||
- description: "Capture memory dump"
|
||||
commands:
|
||||
windows: |
|
||||
# Using built-in (requires admin):
|
||||
# Option 1: procdump (Sysinternals)
|
||||
procdump.exe -ma <SUSPECT_PID> C:\IR\Evidence\process_dump.dmp
|
||||
# Option 2: Full memory using winpmem
|
||||
winpmem_mini_x64.exe C:\IR\Evidence\memory.raw
|
||||
# Option 3: Task Manager > Details > right-click > Create dump file
|
||||
- description: "Capture running processes and network state"
|
||||
commands:
|
||||
windows: |
|
||||
# Processes with full paths and command lines
|
||||
Get-Process | Select-Object Id, ProcessName, Path, CommandLine |
|
||||
Export-Csv C:\IR\Evidence\processes.csv -NoTypeInformation
|
||||
# Network connections mapped to processes
|
||||
Get-NetTCPConnection | Select-Object LocalAddress, LocalPort, RemoteAddress, RemotePort, State, OwningProcess |
|
||||
Export-Csv C:\IR\Evidence\netstat.csv -NoTypeInformation
|
||||
# DNS cache
|
||||
Get-DnsClientCache | Export-Csv C:\IR\Evidence\dns_cache.csv -NoTypeInformation
|
||||
# Loaded DLLs for suspect process
|
||||
Get-Process -Id <SUSPECT_PID> | Select-Object -ExpandProperty Modules |
|
||||
Export-Csv C:\IR\Evidence\loaded_modules.csv -NoTypeInformation
|
||||
- description: "Capture relevant event logs"
|
||||
commands:
|
||||
windows: |
|
||||
wevtutil epl Security C:\IR\Evidence\Security.evtx
|
||||
wevtutil epl System C:\IR\Evidence\System.evtx
|
||||
wevtutil epl "Microsoft-Windows-Sysmon/Operational" C:\IR\Evidence\Sysmon.evtx
|
||||
wevtutil epl "Microsoft-Windows-PowerShell/Operational" C:\IR\Evidence\PowerShell.evtx
|
||||
- description: "Hash and catalog the malware sample(s)"
|
||||
commands:
|
||||
windows: |
|
||||
$file = "<SUSPECT_FILE>"
|
||||
$hashes = @{
|
||||
MD5 = (Get-FileHash $file -Algorithm MD5).Hash
|
||||
SHA1 = (Get-FileHash $file -Algorithm SHA1).Hash
|
||||
SHA256 = (Get-FileHash $file -Algorithm SHA256).Hash
|
||||
}
|
||||
$hashes | ConvertTo-Json | Out-File C:\IR\Evidence\malware_hashes.json
|
||||
# Preserve original file
|
||||
Copy-Item $file "C:\IR\Evidence\MALWARE_SAMPLE_$(Get-Date -Format yyyyMMdd_HHmmss)" -Force
|
||||
|
||||
step_3_host_containment:
|
||||
action: "Stop malicious processes and disable persistence"
|
||||
priority: high
|
||||
procedures:
|
||||
- description: "Kill malicious processes"
|
||||
commands:
|
||||
windows: |
|
||||
# Kill by PID (preferred - precise)
|
||||
Stop-Process -Id <SUSPECT_PID> -Force
|
||||
# Kill process tree
|
||||
taskkill /PID <SUSPECT_PID> /T /F
|
||||
# If process respawns, identify and kill the parent/watchdog first
|
||||
- description: "Disable identified persistence mechanisms"
|
||||
commands:
|
||||
windows: |
|
||||
# Disable scheduled task (don't delete yet - evidence)
|
||||
schtasks /Change /TN "<TASK_NAME>" /Disable
|
||||
# Remove Run key entry (backup first)
|
||||
$key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
|
||||
$val = Get-ItemProperty $key -Name "<VALUE_NAME>"
|
||||
$val | Out-File C:\IR\Evidence\removed_runkey.txt
|
||||
Remove-ItemProperty $key -Name "<VALUE_NAME>"
|
||||
# Disable malicious service
|
||||
Set-Service -Name "<SERVICE_NAME>" -StartupType Disabled
|
||||
Stop-Service -Name "<SERVICE_NAME>" -Force
|
||||
- description: "Quarantine malware files (move, don't delete)"
|
||||
commands:
|
||||
windows: |
|
||||
$quarantine = "C:\IR\Quarantine"
|
||||
New-Item -ItemType Directory -Path $quarantine -Force
|
||||
Move-Item "<MALWARE_PATH>" "$quarantine\" -Force
|
||||
# Set restrictive ACL on quarantine folder
|
||||
icacls $quarantine /inheritance:r /grant "SYSTEM:(OI)(CI)F" /grant "Administrators:(OI)(CI)F"
|
||||
|
||||
# =============================================================================
|
||||
# PHASE 3: ERADICATION
|
||||
# =============================================================================
|
||||
|
||||
phase_3_eradication:
|
||||
title: "Eradication"
|
||||
objective: "Remove all traces of the malware and close the attack vector"
|
||||
max_time: "4 hours"
|
||||
|
||||
step_1_full_scan:
|
||||
action: "Comprehensive scan of the affected endpoint"
|
||||
procedures:
|
||||
- description: "Run full antivirus/EDR scan"
|
||||
commands:
|
||||
windows: |
|
||||
# Windows Defender full scan
|
||||
Start-MpScan -ScanType FullScan
|
||||
# Update signatures first
|
||||
Update-MpSignature
|
||||
# Check scan results
|
||||
Get-MpThreatDetection | Select-Object -Last 20
|
||||
- description: "Submit sample to Detonation Chamber for full analysis"
|
||||
check: |
|
||||
Use Submit tab to detonate the sample:
|
||||
- Select target: both (agent + litterbox)
|
||||
- Wait for full behavioral analysis
|
||||
- Review YARA matches, Sigma detections, and network IOCs
|
||||
- Export results for documentation
|
||||
- description: "YARA sweep for related artifacts"
|
||||
commands:
|
||||
windows: |
|
||||
# Scan common malware staging locations
|
||||
# (Requires yara binary on endpoint)
|
||||
yara64.exe -r malware_indicators.yar "C:\Users"
|
||||
yara64.exe -r malware_indicators.yar "C:\ProgramData"
|
||||
yara64.exe -r malware_indicators.yar "C:\Windows\Temp"
|
||||
|
||||
step_2_persistence_removal:
|
||||
action: "Systematically remove all persistence mechanisms"
|
||||
checklist:
|
||||
- location: "Scheduled Tasks"
|
||||
check_command: 'schtasks /query /fo LIST /v | Select-String -Pattern "Task To Run|TaskName"'
|
||||
remediation: "Delete malicious tasks: schtasks /Delete /TN <NAME> /F"
|
||||
- location: "Registry Run Keys"
|
||||
check_command: |
|
||||
Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
|
||||
Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce"
|
||||
Get-ItemProperty "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
|
||||
Get-ItemProperty "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce"
|
||||
Get-ItemProperty "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run"
|
||||
remediation: "Remove-ItemProperty -Path <KEY> -Name <VALUE>"
|
||||
- location: "Services"
|
||||
check_command: 'Get-WmiObject Win32_Service | Where-Object { $_.PathName -notmatch "Windows|Microsoft|Program Files" }'
|
||||
remediation: "sc.exe delete <SERVICE_NAME>"
|
||||
- location: "Startup Folder"
|
||||
check_command: |
|
||||
Get-ChildItem "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup"
|
||||
Get-ChildItem "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup"
|
||||
remediation: "Remove-Item <SHORTCUT_PATH>"
|
||||
- location: "WMI Event Subscriptions"
|
||||
check_command: |
|
||||
Get-WmiObject -Namespace root\subscription -Class __EventFilter
|
||||
Get-WmiObject -Namespace root\subscription -Class CommandLineEventConsumer
|
||||
Get-WmiObject -Namespace root\subscription -Class __FilterToConsumerBinding
|
||||
remediation: "Remove-WmiObject on each malicious subscription"
|
||||
- location: "DLL Hijacking / Side-loading"
|
||||
check_command: "Check for unsigned DLLs in application directories"
|
||||
remediation: "Remove planted DLLs, verify application integrity"
|
||||
- location: "Browser Extensions"
|
||||
check_command: "Review installed extensions in all browsers"
|
||||
remediation: "Remove malicious extensions, reset browser settings"
|
||||
- location: "Hosts File"
|
||||
check_command: 'Get-Content C:\Windows\System32\drivers\etc\hosts'
|
||||
remediation: "Remove malicious entries"
|
||||
|
||||
step_3_close_attack_vector:
|
||||
action: "Address the root cause / initial access vector"
|
||||
vectors:
|
||||
phishing_email:
|
||||
actions:
|
||||
- "Block sender domain/address at mail gateway"
|
||||
- "Search mailboxes for similar emails and purge"
|
||||
- "Block attachment hash at mail filter"
|
||||
- "Report phishing URL to vendor for takedown"
|
||||
exploited_vulnerability:
|
||||
actions:
|
||||
- "Apply security patch immediately"
|
||||
- "If no patch available: apply workaround or disable vulnerable feature"
|
||||
- "Scan for other systems with same vulnerability"
|
||||
compromised_credentials:
|
||||
actions:
|
||||
- "Force password reset for affected accounts"
|
||||
- "Revoke active sessions/tokens"
|
||||
- "Enable MFA if not already active"
|
||||
- "Check for unauthorized access in audit logs"
|
||||
removable_media:
|
||||
actions:
|
||||
- "Scan the media device"
|
||||
- "Review USB device policies"
|
||||
- "Check if autorun was the vector"
|
||||
supply_chain:
|
||||
actions:
|
||||
- "Identify compromised software/update"
|
||||
- "Block update source"
|
||||
- "Rollback to known-good version"
|
||||
- "Notify vendor"
|
||||
drive_by_download:
|
||||
actions:
|
||||
- "Block the malicious URL/domain"
|
||||
- "Check proxy logs for other visitors"
|
||||
- "Update browser/plugin if exploitation was used"
|
||||
|
||||
# =============================================================================
|
||||
# PHASE 4: RECOVERY
|
||||
# =============================================================================
|
||||
|
||||
phase_4_recovery:
|
||||
title: "Recovery"
|
||||
objective: "Restore normal operations with confidence that the threat is eliminated"
|
||||
max_time: "8 hours (varies by scope)"
|
||||
|
||||
step_1_system_validation:
|
||||
action: "Verify the endpoint is clean before returning to production"
|
||||
procedures:
|
||||
- description: "Final scan with updated signatures"
|
||||
commands:
|
||||
windows: |
|
||||
Update-MpSignature
|
||||
Start-MpScan -ScanType FullScan
|
||||
- description: "Verify no persistence remains"
|
||||
check: "Re-run all checks from Phase 3 Step 2"
|
||||
- description: "Verify no C2 communication"
|
||||
commands:
|
||||
windows: |
|
||||
# Monitor network for 15-30 minutes
|
||||
Get-NetTCPConnection -State Established |
|
||||
Where-Object { $_.RemoteAddress -notmatch '^(10\.|172\.(1[6-9]|2|3[01])\.|192\.168\.|127\.)' } |
|
||||
Select-Object RemoteAddress, RemotePort, OwningProcess,
|
||||
@{N='Process';E={(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName}}
|
||||
- description: "Check system file integrity"
|
||||
commands:
|
||||
windows: |
|
||||
sfc /scannow
|
||||
DISM /Online /Cleanup-Image /CheckHealth
|
||||
|
||||
step_2_restore_operations:
|
||||
action: "Reconnect the endpoint and restore normal access"
|
||||
procedures:
|
||||
- description: "Remove network isolation"
|
||||
commands:
|
||||
windows: |
|
||||
Remove-NetFirewallRule -DisplayName "IR-Isolate-Block-All"
|
||||
Remove-NetFirewallRule -DisplayName "IR-Isolate-Block-Inbound"
|
||||
Remove-NetFirewallRule -DisplayName "IR-Allow-Responder"
|
||||
Remove-NetFirewallRule -DisplayName "IR-Allow-Responder-Out"
|
||||
- description: "Re-enable user access"
|
||||
check: |
|
||||
- Unlock affected user accounts
|
||||
- Issue new credentials if compromised
|
||||
- Verify MFA is active
|
||||
- Communicate to user that access is restored
|
||||
- description: "Restore from backup if needed"
|
||||
check: |
|
||||
Only restore from backup if:
|
||||
- Files were encrypted/destroyed
|
||||
- System integrity cannot be verified
|
||||
- Backup predates the compromise
|
||||
IMPORTANT: Verify backup is not also compromised
|
||||
|
||||
step_3_enhanced_monitoring:
|
||||
action: "Increase monitoring on the recovered endpoint"
|
||||
duration: "30 days minimum"
|
||||
procedures:
|
||||
- description: "Enable verbose logging"
|
||||
commands:
|
||||
windows: |
|
||||
# Enable PowerShell script block logging
|
||||
Set-ItemProperty "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" -Name "EnableScriptBlockLogging" -Value 1
|
||||
# Ensure Sysmon is running with full config
|
||||
sc.exe query Sysmon64
|
||||
- description: "Set up alert rules for re-infection indicators"
|
||||
check: |
|
||||
Create watches for:
|
||||
- Same file hashes
|
||||
- Same C2 IPs/domains
|
||||
- Same persistence locations
|
||||
- Same process names/paths
|
||||
- Same user account anomalies
|
||||
- description: "Schedule follow-up review"
|
||||
check: "Set calendar reminder for 7-day and 30-day review"
|
||||
|
||||
# =============================================================================
|
||||
# PHASE 5: POST-INCIDENT
|
||||
# =============================================================================
|
||||
|
||||
phase_5_post_incident:
|
||||
title: "Post-Incident Activity"
|
||||
objective: "Document lessons learned and improve defenses"
|
||||
max_time: "5 business days after resolution"
|
||||
|
||||
step_1_documentation:
|
||||
action: "Create comprehensive incident report"
|
||||
template:
|
||||
sections:
|
||||
- "Executive Summary (non-technical, for management)"
|
||||
- "Timeline of Events (detection through resolution)"
|
||||
- "Technical Analysis (IOCs, TTPs, malware analysis)"
|
||||
- "Impact Assessment (data loss, downtime, financial)"
|
||||
- "Root Cause Analysis (how initial access occurred)"
|
||||
- "Actions Taken (containment, eradication, recovery)"
|
||||
- "Recommendations (preventive measures)"
|
||||
ioc_documentation:
|
||||
format: "STIX/OpenIOC or internal format"
|
||||
include:
|
||||
- "File hashes (MD5, SHA1, SHA256)"
|
||||
- "File names and paths"
|
||||
- "Registry modifications"
|
||||
- "Network indicators (IPs, domains, URLs)"
|
||||
- "MITRE ATT&CK mapping"
|
||||
- "YARA rules (new or updated)"
|
||||
- "Sigma rules (new or updated)"
|
||||
|
||||
step_2_lessons_learned:
|
||||
action: "Conduct post-incident review meeting"
|
||||
discussion_points:
|
||||
- "What detection worked well?"
|
||||
- "What gaps existed in detection or response?"
|
||||
- "How can we reduce time-to-detection?"
|
||||
- "How can we reduce time-to-containment?"
|
||||
- "Were runbooks adequate or do they need updates?"
|
||||
- "Were communication channels effective?"
|
||||
- "Do we need additional tools or training?"
|
||||
|
||||
step_3_improvement_actions:
|
||||
action: "Implement preventive measures"
|
||||
categories:
|
||||
detection:
|
||||
- "Add new Sigma rules for observed TTPs"
|
||||
- "Add YARA rules for new malware variants"
|
||||
- "Update IOC feeds with discovered indicators"
|
||||
- "Tune existing rules to reduce FP/FN"
|
||||
prevention:
|
||||
- "Apply missing patches"
|
||||
- "Harden endpoint configuration"
|
||||
- "Update email filtering rules"
|
||||
- "Review and restrict user permissions"
|
||||
- "Implement application whitelisting"
|
||||
process:
|
||||
- "Update this playbook with lessons learned"
|
||||
- "Conduct tabletop exercise with team"
|
||||
- "Review and update escalation procedures"
|
||||
- "Schedule additional training if gaps identified"
|
||||
|
||||
# =============================================================================
|
||||
# ESCALATION MATRIX
|
||||
# =============================================================================
|
||||
|
||||
escalation:
|
||||
severity_levels:
|
||||
critical:
|
||||
definition: "Active ransomware, wiper, or data destruction in progress"
|
||||
response_time: "15 minutes"
|
||||
notify:
|
||||
- "SOC Manager (immediate)"
|
||||
- "CISO (within 30 min)"
|
||||
- "Legal/Compliance (within 1 hour if data breach)"
|
||||
- "Executive Leadership (within 2 hours)"
|
||||
actions:
|
||||
- "Invoke full incident response team"
|
||||
- "Consider network segment isolation"
|
||||
- "Activate crisis communication plan"
|
||||
high:
|
||||
definition: "Active C2, credential theft, or lateral movement"
|
||||
response_time: "1 hour"
|
||||
notify:
|
||||
- "SOC Manager"
|
||||
- "Endpoint team lead"
|
||||
- "Affected system owner"
|
||||
actions:
|
||||
- "Assign dedicated incident handler"
|
||||
- "Begin containment procedures"
|
||||
medium:
|
||||
definition: "Confirmed malware, no active C2 or spread observed"
|
||||
response_time: "4 hours"
|
||||
notify:
|
||||
- "SOC analyst on duty"
|
||||
- "Endpoint team"
|
||||
actions:
|
||||
- "Queue for next available analyst"
|
||||
- "Monitor for escalation indicators"
|
||||
low:
|
||||
definition: "PUP/Adware, no security impact"
|
||||
response_time: "24 hours"
|
||||
notify:
|
||||
- "Helpdesk/IT support"
|
||||
actions:
|
||||
- "Standard removal procedure"
|
||||
- "No forensic preservation needed"
|
||||
|
||||
# =============================================================================
|
||||
# COMMUNICATION TEMPLATES
|
||||
# =============================================================================
|
||||
|
||||
communication:
|
||||
initial_notification:
|
||||
subject: "[IR-{TICKET_ID}] Malware Detected on {HOSTNAME}"
|
||||
body: |
|
||||
INCIDENT NOTIFICATION
|
||||
---------------------
|
||||
Severity: {SEVERITY}
|
||||
Endpoint: {HOSTNAME} ({IP_ADDRESS})
|
||||
User: {AFFECTED_USER}
|
||||
Detection: {RULE_NAME} ({ENGINE})
|
||||
Time: {DETECTION_TIME}
|
||||
Status: Investigation in progress
|
||||
|
||||
Initial assessment: {MALWARE_TYPE} detected via {DETECTION_METHOD}.
|
||||
Containment actions: {CONTAINMENT_STATUS}
|
||||
|
||||
Next update in: {NEXT_UPDATE_TIME}
|
||||
|
||||
status_update:
|
||||
subject: "[IR-{TICKET_ID}] Status Update - {STATUS}"
|
||||
body: |
|
||||
STATUS UPDATE
|
||||
-------------
|
||||
Current Phase: {PHASE}
|
||||
Actions Taken: {RECENT_ACTIONS}
|
||||
Findings: {KEY_FINDINGS}
|
||||
Next Steps: {PLANNED_ACTIONS}
|
||||
ETA: {ESTIMATED_RESOLUTION}
|
||||
|
||||
resolution:
|
||||
subject: "[IR-{TICKET_ID}] RESOLVED - Malware Incident on {HOSTNAME}"
|
||||
body: |
|
||||
INCIDENT RESOLVED
|
||||
-----------------
|
||||
Resolution: {RESOLUTION_SUMMARY}
|
||||
Root Cause: {ROOT_CAUSE}
|
||||
Impact: {IMPACT_SUMMARY}
|
||||
Duration: {INCIDENT_DURATION}
|
||||
|
||||
Post-incident report will follow within 5 business days.
|
||||
|
||||
# =============================================================================
|
||||
# QUICK REFERENCE: COMMON COMMANDS
|
||||
# =============================================================================
|
||||
|
||||
quick_reference:
|
||||
evidence_collection:
|
||||
memory_dump: "procdump.exe -ma <PID> C:\\IR\\Evidence\\dump.dmp"
|
||||
process_list: "Get-Process | Select Id,ProcessName,Path | Export-Csv procs.csv"
|
||||
network_state: "Get-NetTCPConnection | Export-Csv netstat.csv"
|
||||
autoruns: "autorunsc64.exe -a * -c -h > autoruns.csv"
|
||||
event_logs: "wevtutil epl Security C:\\IR\\Evidence\\Security.evtx"
|
||||
|
||||
containment:
|
||||
kill_process: "Stop-Process -Id <PID> -Force"
|
||||
kill_tree: "taskkill /PID <PID> /T /F"
|
||||
block_outbound: "New-NetFirewallRule -DisplayName 'Block' -Direction Outbound -Action Block"
|
||||
disable_task: "schtasks /Change /TN <NAME> /Disable"
|
||||
disable_service: "Set-Service -Name <SVC> -StartupType Disabled; Stop-Service <SVC> -Force"
|
||||
|
||||
analysis:
|
||||
file_hash: "Get-FileHash -Algorithm SHA256 -Path <FILE>"
|
||||
signature_check: "Get-AuthenticodeSignature -FilePath <FILE>"
|
||||
strings_search: 'Select-String -Path <FILE> -Pattern "http|https|.exe|.dll" -AllMatches'
|
||||
dns_cache: "Get-DnsClientCache"
|
||||
arp_table: "Get-NetNeighbor"
|
||||
|
||||
detonation_chamber:
|
||||
submit_sample: "Use Web UI Submit tab or: .\make.ps1 submit -File <PATH>"
|
||||
check_alerts: ".\make.ps1 alerts"
|
||||
view_services: ".\make.ps1 services"
|
||||
view_logs: ".\make.ps1 logs"
|
||||
@@ -233,6 +233,12 @@ if (Test-Path $venvPython) {
|
||||
}
|
||||
|
||||
# --- 5. Start LitterBox ---
|
||||
# LitterBox MUST run with full admin privileges (SYSTEM + RunLevel Highest)
|
||||
# because its scanners (PE-Sieve, Hollows-Hunter, Moneta) require:
|
||||
# - SeDebugPrivilege (process memory inspection)
|
||||
# - Access to protected process memory
|
||||
# - Kernel driver communication (for some scanners)
|
||||
# Additionally, it must auto-restart on crash (payload analysis can cause instability)
|
||||
Write-Host "`n--- LitterBox ---" -ForegroundColor Cyan
|
||||
$litterboxPython = "$litterboxDir\venv\Scripts\python.exe"
|
||||
|
||||
@@ -242,12 +248,63 @@ if (Test-Path $litterboxPython) {
|
||||
Copy-Item "C:\vagrant_config\litterbox-config.yaml" "$litterboxDir\Config\config.yaml" -Force
|
||||
}
|
||||
|
||||
Register-ServiceTask -Name "LitterBox" -Command $litterboxPython -Arguments "litterbox.py" -WorkingDirectory $litterboxDir
|
||||
Start-Sleep -Seconds 3
|
||||
# Stop existing LitterBox process cleanly before re-registering
|
||||
$existingProc = Get-Process -Name "python*" -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Path -eq $litterboxPython }
|
||||
if ($existingProc) {
|
||||
Write-Host "[*] Stopping existing LitterBox process (PID: $($existingProc.Id))..." -ForegroundColor Yellow
|
||||
Stop-Process -Id $existingProc.Id -Force -ErrorAction SilentlyContinue
|
||||
Start-Sleep -Seconds 2
|
||||
}
|
||||
|
||||
Write-Host " URL: http://localhost:1337" -ForegroundColor Gray
|
||||
# Unregister previous task
|
||||
Unregister-ScheduledTask -TaskName "LitterBox" -Confirm:$false -ErrorAction SilentlyContinue
|
||||
|
||||
# Create CMD wrapper with logging
|
||||
$wrapperPath = "$logsDir\run-LitterBox.cmd"
|
||||
$logPath = "$logsDir\LitterBox.log"
|
||||
$cmdContent = "@echo off & cd /d `"$litterboxDir`" & `"$litterboxPython`" litterbox.py > `"$logPath`" 2>&1"
|
||||
Set-Content -Path $wrapperPath -Value $cmdContent
|
||||
|
||||
# Register scheduled task with SYSTEM privileges, highest run level, and restart policy
|
||||
$action = New-ScheduledTaskAction -Execute "cmd.exe" -Argument "/c `"$wrapperPath`"" -WorkingDirectory $litterboxDir
|
||||
$trigger = New-ScheduledTaskTrigger -AtStartup
|
||||
$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 "LitterBox" -Action $action -Trigger $trigger -Settings $settings -Principal $principal | Out-Null
|
||||
Start-ScheduledTask -TaskName "LitterBox"
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
# Verify LitterBox started successfully with admin privileges
|
||||
$litterboxTask = Get-ScheduledTask -TaskName "LitterBox" -ErrorAction SilentlyContinue
|
||||
if ($litterboxTask -and $litterboxTask.State -eq "Running") {
|
||||
Write-Host "[+] LitterBox registered and running (SYSTEM, RunLevel=Highest)" -ForegroundColor Green
|
||||
Write-Host " Principal: SYSTEM (full admin privileges)" -ForegroundColor Gray
|
||||
Write-Host " RestartPolicy: 5 retries, 1-min interval" -ForegroundColor Gray
|
||||
Write-Host " URL: http://localhost:1337" -ForegroundColor Gray
|
||||
} else {
|
||||
Write-Host "[!] LitterBox task registered but may not be running yet" -ForegroundColor Yellow
|
||||
Write-Host " Check logs: $logPath" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# Verify API is responding
|
||||
Start-Sleep -Seconds 3
|
||||
try {
|
||||
$null = Invoke-WebRequest -Uri "http://127.0.0.1:1337" -UseBasicParsing -TimeoutSec 5
|
||||
Write-Host "[+] LitterBox API verified: responding on port 1337" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "[!] LitterBox API not yet responding (may need more startup time)" -ForegroundColor Yellow
|
||||
}
|
||||
} else {
|
||||
Write-Host "[!] LitterBox Python venv not found - skipping" -ForegroundColor Yellow
|
||||
Write-Host "[!] LitterBox Python venv not found at $litterboxPython - skipping" -ForegroundColor Yellow
|
||||
Write-Host " Run 'vagrant provision --provision-with litterbox' to install" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# --- 6. Start Detonation Chamber UI ---
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
# install-beaconeye.ps1
|
||||
# Downloads and installs BeaconEye (CobaltStrike beacon memory scanner)
|
||||
#
|
||||
# Source: https://github.com/CCob/BeaconEye
|
||||
#
|
||||
# BeaconEye scans process memory for CobaltStrike beacon configurations:
|
||||
# - Identifies active beacons in memory
|
||||
# - Extracts beacon config (C2 servers, sleep time, jitter, etc.)
|
||||
# - Works against sleep-masked and encoded beacons
|
||||
# - Supports scanning specific PIDs or all processes
|
||||
#
|
||||
# Expected path: C:\tools\BeaconEye\BeaconEye.exe
|
||||
#
|
||||
# Run as Administrator
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
Write-Host "=== Installing BeaconEye ===" -ForegroundColor Cyan
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
$installDir = "C:\tools\BeaconEye"
|
||||
$binDir = "$installDir"
|
||||
$exePath = "$binDir\BeaconEye.exe"
|
||||
|
||||
# Check if already installed
|
||||
if (Test-Path $exePath) {
|
||||
Write-Host "[+] BeaconEye already installed at $exePath" -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Path $binDir -Force | Out-Null
|
||||
|
||||
# --- Try downloading pre-built release from GitHub ---
|
||||
$downloaded = $false
|
||||
$releaseUrls = @(
|
||||
"https://github.com/CCob/BeaconEye/releases/latest/download/BeaconEye.zip",
|
||||
"https://github.com/CCob/BeaconEye/releases/download/v1.0/BeaconEye.zip",
|
||||
"https://github.com/CCob/BeaconEye/releases/latest/download/BeaconEye-net6.0-win-x64.zip"
|
||||
)
|
||||
|
||||
foreach ($url in $releaseUrls) {
|
||||
if ($downloaded) { break }
|
||||
try {
|
||||
Write-Host "[*] Trying: $url" -ForegroundColor Gray
|
||||
$zipPath = "$env:TEMP\BeaconEye.zip"
|
||||
Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing -TimeoutSec 30
|
||||
|
||||
# Extract
|
||||
$extractDir = "$env:TEMP\BeaconEye_extract"
|
||||
Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Expand-Archive -Path $zipPath -DestinationPath $extractDir -Force
|
||||
|
||||
# Find the executable
|
||||
$foundExe = Get-ChildItem -Path $extractDir -Recurse -Filter "BeaconEye.exe" | Select-Object -First 1
|
||||
if ($foundExe) {
|
||||
# Copy all files from the same directory (includes dependencies)
|
||||
Copy-Item -Path "$($foundExe.DirectoryName)\*" -Destination $binDir -Recurse -Force
|
||||
$downloaded = $true
|
||||
Write-Host "[+] BeaconEye downloaded from release" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[!] BeaconEye.exe not found in archive" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Cleanup
|
||||
Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
|
||||
} catch {
|
||||
Write-Host "[!] Download failed: $_" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# --- Fallback: build from source ---
|
||||
if (-not $downloaded) {
|
||||
Write-Host "[*] Pre-built release not available, trying to build from source..." -ForegroundColor Yellow
|
||||
$dotnet = Get-Command dotnet -ErrorAction SilentlyContinue
|
||||
$git = Get-Command git -ErrorAction SilentlyContinue
|
||||
|
||||
if ($dotnet -and $git) {
|
||||
try {
|
||||
$srcDir = "$env:TEMP\BeaconEye_src"
|
||||
Remove-Item $srcDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Write-Host "[*] Cloning BeaconEye repository..." -ForegroundColor Yellow
|
||||
& git clone --depth 1 "https://github.com/CCob/BeaconEye.git" $srcDir 2>$null
|
||||
|
||||
$csproj = Get-ChildItem -Path $srcDir -Recurse -Filter "BeaconEye.csproj" | Select-Object -First 1
|
||||
if (-not $csproj) {
|
||||
# Try .sln file
|
||||
$sln = Get-ChildItem -Path $srcDir -Recurse -Filter "*.sln" | Select-Object -First 1
|
||||
if ($sln) {
|
||||
Write-Host "[*] Building BeaconEye solution..." -ForegroundColor Yellow
|
||||
& dotnet publish $sln.FullName -c Release -o $binDir --self-contained true -r win-x64 2>$null
|
||||
}
|
||||
} else {
|
||||
Write-Host "[*] Building BeaconEye project..." -ForegroundColor Yellow
|
||||
& dotnet publish $csproj.FullName -c Release -o $binDir --self-contained true -r win-x64 2>$null
|
||||
}
|
||||
|
||||
if (Test-Path $exePath) {
|
||||
$downloaded = $true
|
||||
Write-Host "[+] BeaconEye built from source" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[!] Build completed but BeaconEye.exe not found at expected path" -ForegroundColor Yellow
|
||||
# List what was produced
|
||||
Get-ChildItem -Path $binDir -Filter "*.exe" | ForEach-Object {
|
||||
Write-Host " Found: $($_.Name)" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
|
||||
Remove-Item $srcDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
} catch {
|
||||
Write-Host "[!] Build from source failed: $_" -ForegroundColor Yellow
|
||||
}
|
||||
} else {
|
||||
if (-not $dotnet) { Write-Host "[!] dotnet SDK not found" -ForegroundColor Yellow }
|
||||
if (-not $git) { Write-Host "[!] git not found" -ForegroundColor Yellow }
|
||||
Write-Host "[!] Cannot build from source without dotnet SDK and git" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# --- Add Windows Defender exclusion ---
|
||||
if (Test-Path $exePath) {
|
||||
try {
|
||||
Add-MpPreference -ExclusionPath $installDir -ErrorAction SilentlyContinue
|
||||
Write-Host "[+] Added Defender exclusion for $installDir" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "[!] Could not add Defender exclusion (non-critical)" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# --- Summary ---
|
||||
Write-Host ""
|
||||
Write-Host "=== BeaconEye Installation Summary ===" -ForegroundColor Cyan
|
||||
if (Test-Path $exePath) {
|
||||
Write-Host "[+] BeaconEye: INSTALLED at $exePath" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host " Usage:" -ForegroundColor White
|
||||
Write-Host " BeaconEye.exe scan # Scan all processes" -ForegroundColor DarkGray
|
||||
Write-Host " BeaconEye.exe scan --pid 1234 # Scan specific PID" -ForegroundColor DarkGray
|
||||
} else {
|
||||
Write-Host "[-] BeaconEye: NOT INSTALLED" -ForegroundColor Red
|
||||
Write-Host " Manual install: place BeaconEye.exe at $exePath" -ForegroundColor Red
|
||||
}
|
||||
Write-Host ""
|
||||
@@ -0,0 +1,202 @@
|
||||
# install-scanner-tools.ps1
|
||||
# Downloads and installs ThreatCheck + DefenderCheck (AV signature scanning tools)
|
||||
#
|
||||
# ThreatCheck (by rasta-mouse):
|
||||
# - Identifies exact byte sequences that trigger AV/AMSI detection
|
||||
# - Supports Defender and AMSI scan engines
|
||||
# - Binary splitting approach to pinpoint signature matches
|
||||
#
|
||||
# DefenderCheck (by matterpreter):
|
||||
# - Similar byte-splitting approach specifically for Windows Defender
|
||||
# - Predecessor to ThreatCheck, still useful for quick checks
|
||||
#
|
||||
# Expected paths after install:
|
||||
# C:\tools\ThreatCheck\bin\ThreatCheck.exe
|
||||
# C:\tools\DefenderCheck\bin\DefenderCheck.exe
|
||||
#
|
||||
# Run as Administrator
|
||||
|
||||
$ErrorActionPreference = "Continue"
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
Write-Host "=== Installing Scanner Tools (ThreatCheck + DefenderCheck) ===" -ForegroundColor Cyan
|
||||
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
|
||||
# --- ThreatCheck ---
|
||||
$tcInstallDir = "C:\tools\ThreatCheck"
|
||||
$tcBinDir = "$tcInstallDir\bin"
|
||||
$tcExe = "$tcBinDir\ThreatCheck.exe"
|
||||
|
||||
if (Test-Path $tcExe) {
|
||||
Write-Host "[+] ThreatCheck already installed at $tcExe" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[*] Installing ThreatCheck..." -ForegroundColor Yellow
|
||||
|
||||
New-Item -ItemType Directory -Path $tcBinDir -Force | Out-Null
|
||||
|
||||
# Try downloading pre-built release from GitHub
|
||||
$tcDownloaded = $false
|
||||
$tcReleaseUrls = @(
|
||||
"https://github.com/rasta-mouse/ThreatCheck/releases/latest/download/ThreatCheck.zip",
|
||||
"https://github.com/rasta-mouse/ThreatCheck/releases/download/v1.0.0/ThreatCheck.zip"
|
||||
)
|
||||
|
||||
foreach ($url in $tcReleaseUrls) {
|
||||
if ($tcDownloaded) { break }
|
||||
try {
|
||||
Write-Host "[*] Trying: $url" -ForegroundColor Gray
|
||||
$zipPath = "$env:TEMP\ThreatCheck.zip"
|
||||
Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing -TimeoutSec 30
|
||||
Expand-Archive -Path $zipPath -DestinationPath "$env:TEMP\ThreatCheck_extract" -Force
|
||||
|
||||
# Find ThreatCheck.exe in extracted contents (may be nested)
|
||||
$foundExe = Get-ChildItem -Path "$env:TEMP\ThreatCheck_extract" -Recurse -Filter "ThreatCheck.exe" | Select-Object -First 1
|
||||
if ($foundExe) {
|
||||
# Copy all files from the same directory (includes dependencies)
|
||||
Copy-Item -Path "$($foundExe.DirectoryName)\*" -Destination $tcBinDir -Recurse -Force
|
||||
$tcDownloaded = $true
|
||||
Write-Host "[+] ThreatCheck downloaded from release" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[!] ThreatCheck.exe not found in archive" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Cleanup
|
||||
Remove-Item "$env:TEMP\ThreatCheck_extract" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
|
||||
} catch {
|
||||
Write-Host "[!] Download failed: $_" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# Fallback: build from source if dotnet SDK is available
|
||||
if (-not $tcDownloaded) {
|
||||
Write-Host "[*] Pre-built release not available, trying to build from source..." -ForegroundColor Yellow
|
||||
$dotnet = Get-Command dotnet -ErrorAction SilentlyContinue
|
||||
if ($dotnet) {
|
||||
try {
|
||||
$tcSrcDir = "$env:TEMP\ThreatCheck_src"
|
||||
Remove-Item $tcSrcDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
git clone --depth 1 "https://github.com/rasta-mouse/ThreatCheck.git" $tcSrcDir 2>$null
|
||||
|
||||
$csproj = Get-ChildItem -Path $tcSrcDir -Recurse -Filter "ThreatCheck.csproj" | Select-Object -First 1
|
||||
if ($csproj) {
|
||||
Write-Host "[*] Building ThreatCheck with dotnet..." -ForegroundColor Yellow
|
||||
& dotnet publish $csproj.FullName -c Release -o $tcBinDir --self-contained false 2>$null
|
||||
if (Test-Path $tcExe) {
|
||||
$tcDownloaded = $true
|
||||
Write-Host "[+] ThreatCheck built from source" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
Remove-Item $tcSrcDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
} catch {
|
||||
Write-Host "[!] Build from source failed: $_" -ForegroundColor Yellow
|
||||
}
|
||||
} else {
|
||||
Write-Host "[!] dotnet SDK not found - cannot build from source" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $tcDownloaded) {
|
||||
Write-Host "[!] ThreatCheck installation FAILED - no download source available" -ForegroundColor Red
|
||||
Write-Host " Manual install: place ThreatCheck.exe at $tcExe" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
# --- DefenderCheck ---
|
||||
$dcInstallDir = "C:\tools\DefenderCheck"
|
||||
$dcBinDir = "$dcInstallDir\bin"
|
||||
$dcExe = "$dcBinDir\DefenderCheck.exe"
|
||||
|
||||
if (Test-Path $dcExe) {
|
||||
Write-Host "[+] DefenderCheck already installed at $dcExe" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[*] Installing DefenderCheck..." -ForegroundColor Yellow
|
||||
|
||||
New-Item -ItemType Directory -Path $dcBinDir -Force | Out-Null
|
||||
|
||||
# Try downloading pre-built release from GitHub
|
||||
$dcDownloaded = $false
|
||||
$dcReleaseUrls = @(
|
||||
"https://github.com/matterpreter/DefenderCheck/releases/latest/download/DefenderCheck.zip",
|
||||
"https://github.com/matterpreter/DefenderCheck/releases/latest/download/DefenderCheck.exe"
|
||||
)
|
||||
|
||||
foreach ($url in $dcReleaseUrls) {
|
||||
if ($dcDownloaded) { break }
|
||||
try {
|
||||
Write-Host "[*] Trying: $url" -ForegroundColor Gray
|
||||
if ($url.EndsWith(".zip")) {
|
||||
$zipPath = "$env:TEMP\DefenderCheck.zip"
|
||||
Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing -TimeoutSec 30
|
||||
Expand-Archive -Path $zipPath -DestinationPath "$env:TEMP\DefenderCheck_extract" -Force
|
||||
|
||||
$foundExe = Get-ChildItem -Path "$env:TEMP\DefenderCheck_extract" -Recurse -Filter "DefenderCheck.exe" | Select-Object -First 1
|
||||
if ($foundExe) {
|
||||
Copy-Item -Path "$($foundExe.DirectoryName)\*" -Destination $dcBinDir -Recurse -Force
|
||||
$dcDownloaded = $true
|
||||
Write-Host "[+] DefenderCheck downloaded from release" -ForegroundColor Green
|
||||
}
|
||||
Remove-Item "$env:TEMP\DefenderCheck_extract" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $zipPath -Force -ErrorAction SilentlyContinue
|
||||
} else {
|
||||
# Direct exe download
|
||||
Invoke-WebRequest -Uri $url -OutFile $dcExe -UseBasicParsing -TimeoutSec 30
|
||||
if (Test-Path $dcExe) {
|
||||
$dcDownloaded = $true
|
||||
Write-Host "[+] DefenderCheck downloaded directly" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Host "[!] Download failed: $_" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# Fallback: build from source
|
||||
if (-not $dcDownloaded) {
|
||||
Write-Host "[*] Pre-built release not available, trying to build from source..." -ForegroundColor Yellow
|
||||
$dotnet = Get-Command dotnet -ErrorAction SilentlyContinue
|
||||
if ($dotnet) {
|
||||
try {
|
||||
$dcSrcDir = "$env:TEMP\DefenderCheck_src"
|
||||
Remove-Item $dcSrcDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
git clone --depth 1 "https://github.com/matterpreter/DefenderCheck.git" $dcSrcDir 2>$null
|
||||
|
||||
$csproj = Get-ChildItem -Path $dcSrcDir -Recurse -Filter "DefenderCheck.csproj" | Select-Object -First 1
|
||||
if ($csproj) {
|
||||
Write-Host "[*] Building DefenderCheck with dotnet..." -ForegroundColor Yellow
|
||||
& dotnet publish $csproj.FullName -c Release -o $dcBinDir --self-contained false 2>$null
|
||||
if (Test-Path $dcExe) {
|
||||
$dcDownloaded = $true
|
||||
Write-Host "[+] DefenderCheck built from source" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
Remove-Item $dcSrcDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||
} catch {
|
||||
Write-Host "[!] Build from source failed: $_" -ForegroundColor Yellow
|
||||
}
|
||||
} else {
|
||||
Write-Host "[!] dotnet SDK not found - cannot build from source" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $dcDownloaded) {
|
||||
Write-Host "[!] DefenderCheck installation FAILED - no download source available" -ForegroundColor Red
|
||||
Write-Host " Manual install: place DefenderCheck.exe at $dcExe" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
# --- Summary ---
|
||||
Write-Host ""
|
||||
Write-Host "=== Scanner Tools Installation Summary ===" -ForegroundColor Cyan
|
||||
if (Test-Path $tcExe) {
|
||||
Write-Host "[+] ThreatCheck: INSTALLED at $tcExe" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[-] ThreatCheck: NOT INSTALLED" -ForegroundColor Red
|
||||
}
|
||||
if (Test-Path $dcExe) {
|
||||
Write-Host "[+] DefenderCheck: INSTALLED at $dcExe" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "[-] DefenderCheck: NOT INSTALLED" -ForegroundColor Red
|
||||
}
|
||||
Write-Host ""
|
||||
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 169 KiB |
@@ -765,8 +765,13 @@ def _find_service_launch_config():
|
||||
configs["litterbox"] = {"exe": py_exe, "args": "litterbox.py", "cwd": lb_dir}
|
||||
break
|
||||
|
||||
# Fibratus - Windows Service
|
||||
configs["fibratus"] = {"service": "fibratus"}
|
||||
# Fibratus - Windows Service (with exe path for auto-registration if service is missing)
|
||||
fibratus_exe = None
|
||||
for exe_path in [r"C:\Program Files\Fibratus\Bin\fibratus.exe", r"C:\Program Files\Fibratus\fibratus.exe"]:
|
||||
if os.path.isfile(exe_path):
|
||||
fibratus_exe = exe_path
|
||||
break
|
||||
configs["fibratus"] = {"service": "fibratus", "exe": fibratus_exe}
|
||||
|
||||
# Sysmon - Windows Service
|
||||
configs["sysmon"] = {"service": "Sysmon64"}
|
||||
@@ -798,7 +803,38 @@ def api_service_launch():
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return jsonify({"error": f"Failed to start service: {result.stderr.strip()}"}), 500
|
||||
# Service might not be registered yet — try to install it first
|
||||
exe_path = config.get("exe")
|
||||
if exe_path and os.path.isfile(exe_path) and "NoServiceFoundForGivenName" in result.stderr:
|
||||
# Attempt to register the service via the executable
|
||||
install_result = subprocess.run(
|
||||
[exe_path, "install-service"],
|
||||
capture_output=True, text=True, timeout=15
|
||||
)
|
||||
if install_result.returncode == 0:
|
||||
# Set to automatic and start
|
||||
subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command",
|
||||
f"Set-Service -Name '{svc_name}' -StartupType Automatic -ErrorAction SilentlyContinue"],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
start_result = subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command",
|
||||
f"Start-Service -Name '{svc_name}' -ErrorAction Stop"],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if start_result.returncode == 0:
|
||||
return jsonify({"success": True, "message": f"Service '{svc_name}' registered and started"})
|
||||
else:
|
||||
return jsonify({"error": f"Service registered but failed to start: {start_result.stderr.strip()}"}), 500
|
||||
else:
|
||||
return jsonify({"error": f"Service not found and registration failed: {install_result.stderr.strip() or install_result.stdout.strip()}"}), 500
|
||||
elif exe_path and not os.path.isfile(exe_path):
|
||||
return jsonify({"error": f"Service '{svc_name}' not found and executable not installed. Expected at: {exe_path}"}), 500
|
||||
elif not exe_path:
|
||||
return jsonify({"error": f"Service '{svc_name}' not found and no executable path configured for auto-registration."}), 500
|
||||
else:
|
||||
return jsonify({"error": f"Failed to start service: {result.stderr.strip()}"}), 500
|
||||
return jsonify({"success": True, "message": f"Service '{svc_name}' started"})
|
||||
|
||||
# Process launch
|
||||
@@ -1097,10 +1133,92 @@ def api_alerts():
|
||||
|
||||
@app.route("/api/processes")
|
||||
def api_processes():
|
||||
"""Get process tree built from alerts."""
|
||||
"""Get process tree built from alerts.
|
||||
|
||||
Query params:
|
||||
max: Maximum number of processes to return (default: 200, 0=unlimited)
|
||||
min_threats: Minimum threat count to include (default: 1)
|
||||
sort: Sort order - 'threats' (default), 'recent', 'severity'
|
||||
detonated: If 'true', only return detonated processes
|
||||
include_parents: If 'true' (default), include parent processes for context
|
||||
"""
|
||||
max_procs = request.args.get("max", 200, type=int)
|
||||
min_threats = request.args.get("min_threats", 1, type=int)
|
||||
sort_by = request.args.get("sort", "threats")
|
||||
detonated_only = request.args.get("detonated", "").lower() == "true"
|
||||
include_parents = request.args.get("include_parents", "true").lower() != "false"
|
||||
|
||||
with store_lock:
|
||||
processes = events_store.get("processes", {})
|
||||
return jsonify(processes)
|
||||
all_processes = events_store.get("processes", {})
|
||||
|
||||
if not all_processes:
|
||||
return jsonify({})
|
||||
|
||||
# Filter by minimum threats
|
||||
filtered = {
|
||||
pid: proc for pid, proc in all_processes.items()
|
||||
if (proc.get("activity", {}).get("threats", 0) >= min_threats)
|
||||
}
|
||||
|
||||
# Filter by detonated if requested
|
||||
if detonated_only:
|
||||
filtered = {pid: proc for pid, proc in filtered.items() if proc.get("detonated")}
|
||||
|
||||
# Sort to prioritize interesting processes
|
||||
severity_order = {"critical": 4, "high": 3, "medium": 2, "low": 1, "unknown": 0}
|
||||
|
||||
def sort_key(item):
|
||||
pid, proc = item
|
||||
if sort_by == "recent":
|
||||
return proc.get("last_seen", "")
|
||||
elif sort_by == "severity":
|
||||
max_sev = 0
|
||||
for alert in proc.get("alerts", []):
|
||||
sev = severity_order.get(alert.get("severity", "unknown"), 0)
|
||||
if sev > max_sev:
|
||||
max_sev = sev
|
||||
return (max_sev, proc.get("activity", {}).get("threats", 0))
|
||||
else: # threats (default)
|
||||
return proc.get("activity", {}).get("threats", 0)
|
||||
|
||||
sorted_procs = sorted(filtered.items(), key=sort_key, reverse=True)
|
||||
|
||||
# Apply limit (0 = no limit)
|
||||
if max_procs > 0:
|
||||
selected_pids = set(pid for pid, _ in sorted_procs[:max_procs])
|
||||
|
||||
# Include parent processes for graph context (not counted toward limit)
|
||||
if include_parents:
|
||||
parents_to_add = set()
|
||||
for pid in list(selected_pids):
|
||||
proc = all_processes.get(pid, {})
|
||||
ppid = proc.get("parent_pid")
|
||||
if ppid is not None:
|
||||
ppid_str = str(ppid)
|
||||
if ppid_str in all_processes and ppid_str not in selected_pids:
|
||||
parents_to_add.add(ppid_str)
|
||||
selected_pids.update(parents_to_add)
|
||||
|
||||
result = {pid: proc for pid, proc in all_processes.items() if pid in selected_pids}
|
||||
else:
|
||||
result = filtered
|
||||
|
||||
# Strip the full alert objects from response to reduce payload size
|
||||
# (keep only essential fields for graph rendering)
|
||||
compact_result = {}
|
||||
for pid, proc in result.items():
|
||||
compact_proc = dict(proc)
|
||||
# Reduce alerts to lightweight format (just timestamp + severity for time filtering)
|
||||
if compact_proc.get("alerts"):
|
||||
compact_proc["alerts"] = [
|
||||
{"timestamp": a.get("timestamp", ""), "severity": a.get("severity", "unknown"),
|
||||
"rule_name": a.get("rule_name", ""), "category": a.get("category", "")}
|
||||
for a in compact_proc["alerts"]
|
||||
]
|
||||
# Remove raw field from alerts to save bandwidth
|
||||
compact_result[pid] = compact_proc
|
||||
|
||||
return jsonify(compact_result)
|
||||
|
||||
|
||||
@app.route("/api/sysmon")
|
||||
@@ -1117,8 +1235,46 @@ def api_sysmon():
|
||||
|
||||
@app.route("/api/sysmon/stats")
|
||||
def api_sysmon_stats():
|
||||
"""Get Sysmon event counts by type."""
|
||||
"""Get Sysmon event counts by type, with diagnostic info."""
|
||||
try:
|
||||
# First check if the event log channel exists and get record count
|
||||
diag_cmd = (
|
||||
"$log = Get-WinEvent -ListLog 'Microsoft-Windows-Sysmon/Operational' -ErrorAction SilentlyContinue; "
|
||||
"if ($log) { @{Exists=$true; RecordCount=$log.RecordCount; Enabled=$log.IsEnabled; LogMode=$log.LogMode} | ConvertTo-Json -Compress } "
|
||||
"else { @{Exists=$false} | ConvertTo-Json -Compress }"
|
||||
)
|
||||
diag_result = subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command", diag_cmd],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
diag = {}
|
||||
if diag_result.stdout.strip():
|
||||
diag = json.loads(diag_result.stdout.strip())
|
||||
|
||||
if not diag.get("Exists"):
|
||||
return jsonify({
|
||||
"online": False,
|
||||
"stats": [],
|
||||
"diagnostic": "Event log 'Microsoft-Windows-Sysmon/Operational' does not exist. Sysmon may not be properly installed.",
|
||||
})
|
||||
|
||||
if not diag.get("Enabled"):
|
||||
return jsonify({
|
||||
"online": False,
|
||||
"stats": [],
|
||||
"diagnostic": "Sysmon event log exists but is disabled.",
|
||||
})
|
||||
|
||||
record_count = diag.get("RecordCount", 0)
|
||||
if record_count == 0:
|
||||
return jsonify({
|
||||
"online": True,
|
||||
"stats": [],
|
||||
"diagnostic": "Sysmon event log is empty (0 records). Service is running but no events have been logged yet. Check Sysmon config.",
|
||||
"record_count": 0,
|
||||
})
|
||||
|
||||
# Log exists with records — query stats
|
||||
result = subprocess.run(
|
||||
["powershell", "-NoProfile", "-Command",
|
||||
"Get-WinEvent -LogName 'Microsoft-Windows-Sysmon/Operational' -MaxEvents 500 -ErrorAction SilentlyContinue | "
|
||||
@@ -1147,10 +1303,17 @@ def api_sysmon_stats():
|
||||
"name": event_names.get(eid, f"Event {eid}"),
|
||||
"count": item.get("Count", 0),
|
||||
})
|
||||
return jsonify({"online": True, "stats": stats})
|
||||
return jsonify({"online": True, "stats": stats, "record_count": record_count})
|
||||
|
||||
# Command returned empty despite records existing
|
||||
return jsonify({
|
||||
"online": True,
|
||||
"stats": [],
|
||||
"diagnostic": f"Event log has {record_count} records but query returned no results. Possible permission issue.",
|
||||
"record_count": record_count,
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"online": False, "error": str(e), "stats": []})
|
||||
return jsonify({"online": False, "stats": []})
|
||||
|
||||
|
||||
def _read_sysmon_events(max_events=100, since=None, pid=None, event_id=None):
|
||||
@@ -1441,6 +1604,24 @@ def api_submit():
|
||||
except Exception as e:
|
||||
results["litterbox"] = {"status": 502, "error": str(e)}
|
||||
|
||||
# --- Beacon Scanning (async, after agent execution) ---
|
||||
beacon_tools_available = os.path.isfile(HUNT_SLEEPING_BEACONS_EXE) or os.path.isfile(BEACONEYE_EXE)
|
||||
if agent_pid and beacon_tools_available:
|
||||
_run_beacon_scans_async(agent_pid, file_sha256)
|
||||
results["beacon_scan"] = {
|
||||
"triggered": True,
|
||||
"tools": [],
|
||||
}
|
||||
if os.path.isfile(HUNT_SLEEPING_BEACONS_EXE):
|
||||
results["beacon_scan"]["tools"].append("Hunt-Sleeping-Beacons")
|
||||
if os.path.isfile(BEACONEYE_EXE):
|
||||
results["beacon_scan"]["tools"].append("BeaconEye")
|
||||
else:
|
||||
results["beacon_scan"] = {
|
||||
"triggered": False,
|
||||
"reason": "No PID available" if not agent_pid else "Beacon tools not installed",
|
||||
}
|
||||
|
||||
# Include file metadata in response
|
||||
results["file_info"] = {
|
||||
"name": filename,
|
||||
@@ -1475,8 +1656,17 @@ def api_detonation_results():
|
||||
|
||||
results = {"sha256": sha256, "pid": pid, "ready": {}}
|
||||
|
||||
# --- Check LitterBox availability first ---
|
||||
litterbox_online = False
|
||||
try:
|
||||
r = requests.get(LITTERBOX_API, timeout=2)
|
||||
litterbox_online = r.status_code == 200
|
||||
except Exception:
|
||||
pass
|
||||
results["litterbox_online"] = litterbox_online
|
||||
|
||||
# --- LitterBox Static Results ---
|
||||
if lb_hash:
|
||||
if lb_hash and litterbox_online:
|
||||
try:
|
||||
r = requests.get(f"{LITTERBOX_API}/api/results/static/{lb_hash}", timeout=5)
|
||||
if r.status_code == 200:
|
||||
@@ -1486,31 +1676,40 @@ def api_detonation_results():
|
||||
results["ready"]["static"] = False
|
||||
except Exception:
|
||||
results["ready"]["static"] = False
|
||||
elif lb_hash and not litterbox_online:
|
||||
results["ready"]["static"] = True # Don't block polling if LitterBox is offline
|
||||
else:
|
||||
results["ready"]["static"] = True # No hash to look up
|
||||
|
||||
# --- LitterBox Dynamic Results ---
|
||||
if pid:
|
||||
try:
|
||||
r = requests.get(f"{LITTERBOX_API}/api/results/dynamic/{pid}", timeout=5)
|
||||
if r.status_code == 200:
|
||||
results["litterbox_dynamic"] = r.json()
|
||||
results["ready"]["dynamic"] = True
|
||||
else:
|
||||
if litterbox_online:
|
||||
if pid:
|
||||
try:
|
||||
r = requests.get(f"{LITTERBOX_API}/api/results/dynamic/{pid}", timeout=5)
|
||||
if r.status_code == 200:
|
||||
results["litterbox_dynamic"] = r.json()
|
||||
results["ready"]["dynamic"] = True
|
||||
else:
|
||||
results["ready"]["dynamic"] = False
|
||||
except Exception:
|
||||
results["ready"]["dynamic"] = False
|
||||
except Exception:
|
||||
results["ready"]["dynamic"] = False
|
||||
elif lb_hash:
|
||||
try:
|
||||
r = requests.get(f"{LITTERBOX_API}/api/results/dynamic/{lb_hash}", timeout=5)
|
||||
if r.status_code == 200:
|
||||
results["litterbox_dynamic"] = r.json()
|
||||
results["ready"]["dynamic"] = True
|
||||
else:
|
||||
elif lb_hash:
|
||||
try:
|
||||
r = requests.get(f"{LITTERBOX_API}/api/results/dynamic/{lb_hash}", timeout=5)
|
||||
if r.status_code == 200:
|
||||
results["litterbox_dynamic"] = r.json()
|
||||
results["ready"]["dynamic"] = True
|
||||
else:
|
||||
results["ready"]["dynamic"] = False
|
||||
except Exception:
|
||||
results["ready"]["dynamic"] = False
|
||||
except Exception:
|
||||
results["ready"]["dynamic"] = False
|
||||
else:
|
||||
results["ready"]["dynamic"] = True # No target to look up
|
||||
else:
|
||||
results["ready"]["dynamic"] = True # Don't block polling if LitterBox is offline
|
||||
|
||||
# --- LitterBox File Info (includes basic PE info, hashes) ---
|
||||
if lb_hash:
|
||||
if lb_hash and litterbox_online:
|
||||
try:
|
||||
r = requests.get(f"{LITTERBOX_API}/api/results/info/{lb_hash}", timeout=5)
|
||||
if r.status_code == 200:
|
||||
@@ -1518,6 +1717,29 @@ def api_detonation_results():
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# --- Beacon Scan Results (from async cache) ---
|
||||
with _beacon_cache_lock:
|
||||
hsb_result = None
|
||||
beaconeye_result = None
|
||||
if pid:
|
||||
hsb_result = _beacon_results_cache.get(f"hsb_{pid}")
|
||||
beaconeye_result = _beacon_results_cache.get(f"beaconeye_{pid}")
|
||||
if not hsb_result and sha256:
|
||||
hsb_result = _beacon_results_cache.get(f"hsb_{sha256}")
|
||||
if not beaconeye_result and sha256:
|
||||
beaconeye_result = _beacon_results_cache.get(f"beaconeye_{sha256}")
|
||||
|
||||
if hsb_result:
|
||||
results["hunt_sleeping_beacons"] = hsb_result
|
||||
if beaconeye_result:
|
||||
results["beaconeye"] = beaconeye_result
|
||||
|
||||
# Beacon tools status
|
||||
results["beacon_tools"] = {
|
||||
"hsb_installed": os.path.isfile(HUNT_SLEEPING_BEACONS_EXE),
|
||||
"beaconeye_installed": os.path.isfile(BEACONEYE_EXE),
|
||||
}
|
||||
|
||||
# --- Fibratus / Rustinel Alerts matching this detonation ---
|
||||
matching_alerts = []
|
||||
search_terms = set()
|
||||
@@ -1567,7 +1789,22 @@ def api_detonation_results():
|
||||
|
||||
results["fibratus_alerts"] = matching_alerts[:50]
|
||||
results["fibratus_alert_count"] = len(matching_alerts)
|
||||
results["ready"]["fibratus"] = len(matching_alerts) > 0
|
||||
|
||||
# Include EDR service status so frontend can detect offline state early
|
||||
fibratus_online = _is_fibratus_running()
|
||||
rustinel_online = os.path.isdir(RUSTINEL_ALERTS_DIR) and _is_rustinel_running()
|
||||
results["edr_status"] = {
|
||||
"fibratus_online": fibratus_online,
|
||||
"rustinel_online": rustinel_online,
|
||||
}
|
||||
|
||||
# EDR is "ready" if we have alerts OR if both services are offline (no point waiting)
|
||||
if len(matching_alerts) > 0:
|
||||
results["ready"]["fibratus"] = True
|
||||
elif not fibratus_online and not rustinel_online:
|
||||
results["ready"]["fibratus"] = True # Don't block polling if both are offline
|
||||
else:
|
||||
results["ready"]["fibratus"] = False
|
||||
|
||||
return jsonify(results)
|
||||
|
||||
@@ -1775,6 +2012,14 @@ def api_file_hex_write():
|
||||
THREATCHECK_EXE = r"C:\tools\ThreatCheck\bin\ThreatCheck.exe"
|
||||
DEFENDERCHECK_EXE = r"C:\tools\DefenderCheck\bin\DefenderCheck.exe"
|
||||
|
||||
# --- Beacon Scanner Integration ---
|
||||
HUNT_SLEEPING_BEACONS_EXE = r"C:\tools\Hunt-Sleeping-Beacons\Hunt-Sleeping-Beacons.exe"
|
||||
BEACONEYE_EXE = r"C:\tools\BeaconEye\BeaconEye.exe"
|
||||
|
||||
# Cache for beacon scan results (keyed by PID or sha256)
|
||||
_beacon_results_cache = {}
|
||||
_beacon_cache_lock = threading.Lock()
|
||||
|
||||
|
||||
@app.route("/api/scan/threatcheck", methods=["POST"])
|
||||
def api_scan_threatcheck():
|
||||
@@ -1884,9 +2129,220 @@ def api_scan_status():
|
||||
"installed": os.path.isfile(DEFENDERCHECK_EXE),
|
||||
"path": DEFENDERCHECK_EXE,
|
||||
},
|
||||
"hunt_sleeping_beacons": {
|
||||
"installed": os.path.isfile(HUNT_SLEEPING_BEACONS_EXE),
|
||||
"path": HUNT_SLEEPING_BEACONS_EXE,
|
||||
},
|
||||
"beaconeye": {
|
||||
"installed": os.path.isfile(BEACONEYE_EXE),
|
||||
"path": BEACONEYE_EXE,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
# --- Beacon Scanner Endpoints ---
|
||||
|
||||
@app.route("/api/scan/beacons", methods=["POST"])
|
||||
def api_scan_beacons():
|
||||
"""Run Hunt-Sleeping-Beacons on a specific PID or all processes."""
|
||||
data = request.get_json(force=True) if request.is_json else request.form
|
||||
pid = data.get("pid")
|
||||
|
||||
if not os.path.isfile(HUNT_SLEEPING_BEACONS_EXE):
|
||||
return jsonify({"error": "Hunt-Sleeping-Beacons not installed", "path": HUNT_SLEEPING_BEACONS_EXE}), 500
|
||||
|
||||
try:
|
||||
args = [HUNT_SLEEPING_BEACONS_EXE, "--commandline"]
|
||||
if pid:
|
||||
args.extend(["-p", str(pid)])
|
||||
result = subprocess.run(
|
||||
args, capture_output=True, text=True, timeout=60,
|
||||
cwd=os.path.dirname(HUNT_SLEEPING_BEACONS_EXE)
|
||||
)
|
||||
output = (result.stdout or "") + (result.stderr or "")
|
||||
findings = _parse_hsb_output(output)
|
||||
|
||||
scan_result = {
|
||||
"tool": "Hunt-Sleeping-Beacons",
|
||||
"pid": pid,
|
||||
"output": output.strip(),
|
||||
"findings": findings,
|
||||
"suspicious_count": len(findings),
|
||||
"exit_code": result.returncode,
|
||||
}
|
||||
|
||||
# Cache results for polling
|
||||
if pid:
|
||||
with _beacon_cache_lock:
|
||||
key = f"hsb_{pid}"
|
||||
_beacon_results_cache[key] = scan_result
|
||||
|
||||
return jsonify(scan_result)
|
||||
except subprocess.TimeoutExpired:
|
||||
return jsonify({"error": "Hunt-Sleeping-Beacons timed out (60s)"}), 504
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route("/api/scan/beaconeye", methods=["POST"])
|
||||
def api_scan_beaconeye():
|
||||
"""Run BeaconEye to scan process memory for CobaltStrike beacon configs."""
|
||||
data = request.get_json(force=True) if request.is_json else request.form
|
||||
pid = data.get("pid")
|
||||
|
||||
if not os.path.isfile(BEACONEYE_EXE):
|
||||
return jsonify({"error": "BeaconEye not installed", "path": BEACONEYE_EXE}), 500
|
||||
|
||||
try:
|
||||
args = [BEACONEYE_EXE, "scan"]
|
||||
if pid:
|
||||
args = [BEACONEYE_EXE, "scan", "--pid", str(pid)]
|
||||
result = subprocess.run(
|
||||
args, capture_output=True, text=True, timeout=90,
|
||||
cwd=os.path.dirname(BEACONEYE_EXE)
|
||||
)
|
||||
output = (result.stdout or "") + (result.stderr or "")
|
||||
findings = _parse_beaconeye_output(output)
|
||||
|
||||
scan_result = {
|
||||
"tool": "BeaconEye",
|
||||
"pid": pid,
|
||||
"output": output.strip(),
|
||||
"findings": findings,
|
||||
"beacons_found": len(findings),
|
||||
"exit_code": result.returncode,
|
||||
}
|
||||
|
||||
# Cache results for polling
|
||||
if pid:
|
||||
with _beacon_cache_lock:
|
||||
key = f"beaconeye_{pid}"
|
||||
_beacon_results_cache[key] = scan_result
|
||||
|
||||
return jsonify(scan_result)
|
||||
except subprocess.TimeoutExpired:
|
||||
return jsonify({"error": "BeaconEye timed out (90s)"}), 504
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
def _parse_hsb_output(output):
|
||||
"""Parse Hunt-Sleeping-Beacons output into structured findings."""
|
||||
findings = []
|
||||
current = None
|
||||
for line in output.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
if current:
|
||||
findings.append(current)
|
||||
current = None
|
||||
continue
|
||||
# Detect process lines (typically "PID: XXXX ..." or process name lines)
|
||||
if "suspicious" in line.lower() or "ioc" in line.lower() or "beacon" in line.lower():
|
||||
if current is None:
|
||||
current = {"indicators": [], "raw": ""}
|
||||
current["indicators"].append(line)
|
||||
current["raw"] += line + "\n"
|
||||
elif "pid" in line.lower() and ":" in line:
|
||||
if current:
|
||||
findings.append(current)
|
||||
current = {"process": line, "indicators": [], "raw": line + "\n"}
|
||||
elif current:
|
||||
current["raw"] += line + "\n"
|
||||
if any(kw in line.lower() for kw in ["unbacked", "private", "stomping", "spoofing", "apc", "timer", "proxy"]):
|
||||
current["indicators"].append(line)
|
||||
if current:
|
||||
findings.append(current)
|
||||
return findings
|
||||
|
||||
|
||||
def _parse_beaconeye_output(output):
|
||||
"""Parse BeaconEye output into structured beacon findings."""
|
||||
findings = []
|
||||
current = None
|
||||
for line in output.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
# BeaconEye typically outputs beacon configs when found
|
||||
if "beacon" in line.lower() and ("found" in line.lower() or "config" in line.lower() or "pid" in line.lower()):
|
||||
if current:
|
||||
findings.append(current)
|
||||
current = {"summary": line, "config": {}, "raw": line + "\n"}
|
||||
elif current:
|
||||
current["raw"] += line + "\n"
|
||||
# Parse key-value config lines
|
||||
if ":" in line or "=" in line:
|
||||
sep = ":" if ":" in line else "="
|
||||
parts = line.split(sep, 1)
|
||||
if len(parts) == 2:
|
||||
current["config"][parts[0].strip()] = parts[1].strip()
|
||||
if current:
|
||||
findings.append(current)
|
||||
return findings
|
||||
|
||||
|
||||
def _run_beacon_scans_async(pid, sha256):
|
||||
"""Run beacon scans in a background thread after sample execution."""
|
||||
def _scan():
|
||||
import time as _time
|
||||
# Wait a few seconds for the beacon to initialize and enter sleep
|
||||
_time.sleep(5)
|
||||
|
||||
# Hunt-Sleeping-Beacons
|
||||
if os.path.isfile(HUNT_SLEEPING_BEACONS_EXE):
|
||||
try:
|
||||
args = [HUNT_SLEEPING_BEACONS_EXE, "--commandline", "-p", str(pid)]
|
||||
result = subprocess.run(
|
||||
args, capture_output=True, text=True, timeout=60,
|
||||
cwd=os.path.dirname(HUNT_SLEEPING_BEACONS_EXE)
|
||||
)
|
||||
output = (result.stdout or "") + (result.stderr or "")
|
||||
findings = _parse_hsb_output(output)
|
||||
with _beacon_cache_lock:
|
||||
_beacon_results_cache[f"hsb_{pid}"] = {
|
||||
"tool": "Hunt-Sleeping-Beacons",
|
||||
"pid": pid,
|
||||
"output": output.strip(),
|
||||
"findings": findings,
|
||||
"suspicious_count": len(findings),
|
||||
"exit_code": result.returncode,
|
||||
}
|
||||
if sha256:
|
||||
_beacon_results_cache[f"hsb_{sha256}"] = _beacon_results_cache[f"hsb_{pid}"]
|
||||
except Exception as e:
|
||||
with _beacon_cache_lock:
|
||||
_beacon_results_cache[f"hsb_{pid}"] = {"tool": "Hunt-Sleeping-Beacons", "error": str(e)}
|
||||
|
||||
# BeaconEye
|
||||
if os.path.isfile(BEACONEYE_EXE):
|
||||
try:
|
||||
args = [BEACONEYE_EXE, "scan", "--pid", str(pid)]
|
||||
result = subprocess.run(
|
||||
args, capture_output=True, text=True, timeout=90,
|
||||
cwd=os.path.dirname(BEACONEYE_EXE)
|
||||
)
|
||||
output = (result.stdout or "") + (result.stderr or "")
|
||||
findings = _parse_beaconeye_output(output)
|
||||
with _beacon_cache_lock:
|
||||
_beacon_results_cache[f"beaconeye_{pid}"] = {
|
||||
"tool": "BeaconEye",
|
||||
"pid": pid,
|
||||
"output": output.strip(),
|
||||
"findings": findings,
|
||||
"beacons_found": len(findings),
|
||||
"exit_code": result.returncode,
|
||||
}
|
||||
if sha256:
|
||||
_beacon_results_cache[f"beaconeye_{sha256}"] = _beacon_results_cache[f"beaconeye_{pid}"]
|
||||
except Exception as e:
|
||||
with _beacon_cache_lock:
|
||||
_beacon_results_cache[f"beaconeye_{pid}"] = {"tool": "BeaconEye", "error": str(e)}
|
||||
|
||||
thread = threading.Thread(target=_scan, daemon=True)
|
||||
thread.start()
|
||||
|
||||
|
||||
# --- PE Analysis ---
|
||||
# Suspicious API calls grouped by category (IOC indicators)
|
||||
SUSPICIOUS_IMPORTS = {
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
"""
|
||||
Detonation Chamber - Development Server
|
||||
|
||||
Enhanced dev server with:
|
||||
- Flask debug mode (auto-reload on Python changes)
|
||||
- Live-reload for frontend assets (CSS/JS/HTML) via SSE
|
||||
- Auto-opens browser on startup
|
||||
- Mock service endpoints when backend services are unavailable
|
||||
- Colored console output with file change notifications
|
||||
|
||||
Usage:
|
||||
python dev_server.py [--no-open] [--port PORT] [--mock]
|
||||
|
||||
Requires: flask, watchdog, requests (all in requirements.txt)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import signal
|
||||
import argparse
|
||||
import threading
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
from queue import Queue, Empty
|
||||
from datetime import datetime
|
||||
|
||||
# Add parent to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from flask import Response, request as flask_request
|
||||
from watchdog.observers import Observer
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
|
||||
|
||||
# --- Configuration ---
|
||||
DEV_PORT = int(os.environ.get("WEBUI_PORT", "9000"))
|
||||
BASE_DIR = Path(__file__).parent
|
||||
STATIC_DIR = BASE_DIR / "static"
|
||||
TEMPLATES_DIR = BASE_DIR / "templates"
|
||||
|
||||
# SSE clients waiting for reload signals
|
||||
_sse_clients: list[Queue] = []
|
||||
_sse_lock = threading.Lock()
|
||||
|
||||
|
||||
# --- Colors for console ---
|
||||
class Colors:
|
||||
RESET = "\033[0m"
|
||||
BOLD = "\033[1m"
|
||||
DIM = "\033[2m"
|
||||
RED = "\033[31m"
|
||||
GREEN = "\033[32m"
|
||||
YELLOW = "\033[33m"
|
||||
BLUE = "\033[34m"
|
||||
MAGENTA = "\033[35m"
|
||||
CYAN = "\033[36m"
|
||||
WHITE = "\033[97m"
|
||||
|
||||
|
||||
def log(msg, color=Colors.WHITE):
|
||||
ts = datetime.now().strftime("%H:%M:%S")
|
||||
print(f"{Colors.DIM}[{ts}]{Colors.RESET} {color}{msg}{Colors.RESET}")
|
||||
|
||||
|
||||
def log_change(event_type, path):
|
||||
rel = os.path.relpath(path, BASE_DIR)
|
||||
icon = {"modified": "~", "created": "+", "deleted": "-"}.get(event_type, "?")
|
||||
color = {"modified": Colors.YELLOW, "created": Colors.GREEN, "deleted": Colors.RED}.get(event_type, Colors.WHITE)
|
||||
log(f"{icon} {rel}", color)
|
||||
|
||||
|
||||
# --- SSE Live Reload ---
|
||||
LIVERELOAD_JS = """
|
||||
<script id="__dev-livereload">
|
||||
(function() {
|
||||
var es = new EventSource('/__dev/livereload');
|
||||
es.onmessage = function(e) {
|
||||
var data = JSON.parse(e.data);
|
||||
if (data.type === 'reload') {
|
||||
console.log('[dev] Reloading...');
|
||||
location.reload();
|
||||
} else if (data.type === 'css') {
|
||||
console.log('[dev] Refreshing CSS...');
|
||||
var links = document.querySelectorAll('link[rel="stylesheet"]');
|
||||
links.forEach(function(link) {
|
||||
var href = link.href.split('?')[0];
|
||||
link.href = href + '?v=' + Date.now();
|
||||
});
|
||||
}
|
||||
};
|
||||
es.onerror = function() {
|
||||
console.log('[dev] Connection lost, retrying...');
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
"""
|
||||
|
||||
|
||||
def notify_clients(change_type="reload"):
|
||||
"""Send reload signal to all connected SSE clients."""
|
||||
data = json.dumps({"type": change_type, "time": time.time()})
|
||||
with _sse_lock:
|
||||
dead = []
|
||||
for q in _sse_clients:
|
||||
try:
|
||||
q.put_nowait(data)
|
||||
except Exception:
|
||||
dead.append(q)
|
||||
for q in dead:
|
||||
_sse_clients.remove(q)
|
||||
|
||||
|
||||
# --- File Watcher ---
|
||||
class FrontendChangeHandler(FileSystemEventHandler):
|
||||
"""Watch for CSS/JS/HTML changes and trigger live-reload."""
|
||||
|
||||
def __init__(self):
|
||||
self._debounce = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def _should_process(self, path):
|
||||
"""Debounce: ignore rapid successive events for the same file."""
|
||||
now = time.time()
|
||||
with self._lock:
|
||||
last = self._debounce.get(path, 0)
|
||||
if now - last < 0.5:
|
||||
return False
|
||||
self._debounce[path] = now
|
||||
return True
|
||||
|
||||
def _handle(self, event, event_type):
|
||||
if event.is_directory:
|
||||
return
|
||||
path = event.src_path
|
||||
ext = os.path.splitext(path)[1].lower()
|
||||
|
||||
# Only watch relevant file types
|
||||
if ext not in ('.css', '.js', '.html', '.htm', '.png', '.svg', '.ico'):
|
||||
return
|
||||
|
||||
if not self._should_process(path):
|
||||
return
|
||||
|
||||
log_change(event_type, path)
|
||||
|
||||
# CSS-only hot update (no full page reload)
|
||||
if ext == '.css':
|
||||
notify_clients("css")
|
||||
else:
|
||||
notify_clients("reload")
|
||||
|
||||
def on_modified(self, event):
|
||||
self._handle(event, "modified")
|
||||
|
||||
def on_created(self, event):
|
||||
self._handle(event, "created")
|
||||
|
||||
def on_deleted(self, event):
|
||||
self._handle(event, "deleted")
|
||||
|
||||
|
||||
def start_watcher():
|
||||
"""Start watchdog observer for static/ and templates/ directories."""
|
||||
handler = FrontendChangeHandler()
|
||||
observer = Observer()
|
||||
|
||||
watch_dirs = [
|
||||
str(STATIC_DIR),
|
||||
str(TEMPLATES_DIR),
|
||||
]
|
||||
|
||||
for d in watch_dirs:
|
||||
if os.path.exists(d):
|
||||
observer.schedule(handler, d, recursive=True)
|
||||
log(f" Watching: {os.path.relpath(d, BASE_DIR)}/", Colors.DIM)
|
||||
|
||||
observer.start()
|
||||
return observer
|
||||
|
||||
|
||||
# --- Inject dev tools into Flask app ---
|
||||
def setup_dev_routes(app):
|
||||
"""Add development-only routes to the Flask app."""
|
||||
|
||||
@app.route("/__dev/livereload")
|
||||
def dev_livereload():
|
||||
"""SSE endpoint for live-reload notifications."""
|
||||
def stream():
|
||||
q = Queue()
|
||||
with _sse_lock:
|
||||
_sse_clients.append(q)
|
||||
try:
|
||||
# Send initial connected event
|
||||
yield f"data: {json.dumps({'type': 'connected'})}\n\n"
|
||||
while True:
|
||||
try:
|
||||
data = q.get(timeout=30)
|
||||
yield f"data: {data}\n\n"
|
||||
except Empty:
|
||||
# Keep-alive ping
|
||||
yield f": keepalive\n\n"
|
||||
except GeneratorExit:
|
||||
pass
|
||||
finally:
|
||||
with _sse_lock:
|
||||
if q in _sse_clients:
|
||||
_sse_clients.remove(q)
|
||||
|
||||
return Response(stream(), mimetype="text/event-stream",
|
||||
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
|
||||
|
||||
@app.route("/__dev/status")
|
||||
def dev_status():
|
||||
"""Dev server status endpoint."""
|
||||
with _sse_lock:
|
||||
client_count = len(_sse_clients)
|
||||
return json.dumps({
|
||||
"mode": "development",
|
||||
"livereload": True,
|
||||
"connected_clients": client_count,
|
||||
"watched_dirs": ["static/", "templates/"],
|
||||
}), 200, {"Content-Type": "application/json"}
|
||||
|
||||
# Inject livereload script into HTML responses
|
||||
@app.after_request
|
||||
def inject_livereload(response):
|
||||
if (response.content_type
|
||||
and "text/html" in response.content_type
|
||||
and response.status_code == 200):
|
||||
data = response.get_data(as_text=True)
|
||||
if "</body>" in data:
|
||||
data = data.replace("</body>", f"{LIVERELOAD_JS}</body>")
|
||||
response.set_data(data)
|
||||
return response
|
||||
|
||||
log(" Live-reload: enabled (SSE)", Colors.DIM)
|
||||
|
||||
|
||||
def open_browser(port, delay=1.5):
|
||||
"""Open browser after a short delay to let the server start."""
|
||||
def _open():
|
||||
time.sleep(delay)
|
||||
url = f"http://localhost:{port}"
|
||||
log(f"Opening browser: {url}", Colors.CYAN)
|
||||
webbrowser.open(url)
|
||||
t = threading.Thread(target=_open, daemon=True)
|
||||
t.start()
|
||||
|
||||
|
||||
# --- Main ---
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="TDC Development Server")
|
||||
parser.add_argument("--port", type=int, default=DEV_PORT,
|
||||
help=f"Port to run on (default: {DEV_PORT})")
|
||||
parser.add_argument("--no-open", action="store_true",
|
||||
help="Don't auto-open browser")
|
||||
parser.add_argument("--mock", action="store_true",
|
||||
help="Enable mock mode (stub backend APIs)")
|
||||
parser.add_argument("--host", default="127.0.0.1",
|
||||
help="Host to bind to (default: 127.0.0.1)")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Banner
|
||||
print()
|
||||
print(f"{Colors.CYAN}{Colors.BOLD} Transportable Detonation Chamber - Dev Server{Colors.RESET}")
|
||||
print(f"{Colors.DIM} ================================================{Colors.RESET}")
|
||||
print()
|
||||
|
||||
# Set dev environment
|
||||
os.environ["FLASK_DEBUG"] = "1"
|
||||
os.environ["FLASK_ENV"] = "development"
|
||||
|
||||
if args.mock:
|
||||
os.environ["TDC_MOCK_SERVICES"] = "1"
|
||||
log(" Mock mode: ON (backend APIs stubbed)", Colors.YELLOW)
|
||||
|
||||
# Import the app after setting env vars
|
||||
from app import app as flask_app
|
||||
|
||||
# Add dev routes
|
||||
setup_dev_routes(flask_app)
|
||||
|
||||
# Start file watcher
|
||||
observer = start_watcher()
|
||||
|
||||
print()
|
||||
log(f" Server: http://{args.host}:{args.port}", Colors.GREEN)
|
||||
log(f" Mode: development (debug + live-reload)", Colors.DIM)
|
||||
print()
|
||||
print(f"{Colors.DIM} Changes to CSS/JS/HTML will auto-refresh the browser.{Colors.RESET}")
|
||||
print(f"{Colors.DIM} Changes to Python files will restart the server.{Colors.RESET}")
|
||||
print(f"{Colors.DIM} Press Ctrl+C to stop.{Colors.RESET}")
|
||||
print()
|
||||
|
||||
# Auto-open browser
|
||||
if not args.no_open:
|
||||
open_browser(args.port)
|
||||
|
||||
# Run Flask
|
||||
try:
|
||||
flask_app.run(
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
debug=True,
|
||||
use_reloader=True,
|
||||
extra_files=[
|
||||
str(STATIC_DIR / "js" / "app.js"),
|
||||
str(STATIC_DIR / "css" / "style.css"),
|
||||
str(TEMPLATES_DIR / "index.html"),
|
||||
]
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
observer.stop()
|
||||
observer.join()
|
||||
log("Dev server stopped.", Colors.YELLOW)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -3533,6 +3533,31 @@ body.hex-resizing {
|
||||
border-radius: 3px;
|
||||
margin: 2px 2px;
|
||||
}
|
||||
.det-beacon-findings {
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
padding-left: 12px;
|
||||
border-left: 2px solid rgba(251,191,36,0.3);
|
||||
}
|
||||
.det-beacon-finding {
|
||||
padding: 3px 0;
|
||||
font-size: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.02);
|
||||
}
|
||||
.det-beacon-finding:last-child { border-bottom: none; }
|
||||
.det-beacon-proc {
|
||||
color: var(--accent-cyan);
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 500;
|
||||
}
|
||||
.det-beacon-indicators {
|
||||
color: #fbbf24;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
}
|
||||
.det-raw {
|
||||
font-size: 10px;
|
||||
font-family: var(--font-mono);
|
||||
@@ -3805,6 +3830,35 @@ body.hex-resizing {
|
||||
.stats-chip.type-injection { border-color: rgba(239,68,68,0.5); color: #f87171; }
|
||||
.stats-chip.type-access { border-color: rgba(251,191,36,0.4); color: #fbbf24; }
|
||||
.stats-chip.type-other { border-color: var(--border-primary); }
|
||||
|
||||
/* Sysmon diagnostic messages */
|
||||
.sysmon-diagnostic {
|
||||
padding: 12px 20px;
|
||||
font-size: 12px;
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.sysmon-diagnostic::before {
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sysmon-diagnostic.info {
|
||||
color: var(--accent-cyan);
|
||||
background: rgba(34,211,238,0.04);
|
||||
}
|
||||
.sysmon-diagnostic.info::before { content: "\2139\FE0F"; }
|
||||
.sysmon-diagnostic.warning {
|
||||
color: #fbbf24;
|
||||
background: rgba(251,191,36,0.04);
|
||||
}
|
||||
.sysmon-diagnostic.warning::before { content: "\26A0\FE0F"; }
|
||||
.sysmon-diagnostic.error {
|
||||
color: #ef4444;
|
||||
background: rgba(239,68,68,0.04);
|
||||
}
|
||||
.sysmon-diagnostic.error::before { content: "\274C"; }
|
||||
.sysmon-events-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
.sysmon-events-table thead th { position: sticky; top: 0; background: var(--bg-primary); padding: 8px 10px; text-align: left; font-size: 10px; font-weight: 600; text-transform: uppercase; color: var(--text-muted); border-bottom: 1px solid var(--border-primary); }
|
||||
.sysmon-events-table tbody tr { cursor: pointer; transition: background 0.1s; border-bottom: 1px solid var(--border-primary); }
|
||||
@@ -4508,3 +4562,78 @@ body.hex-resizing {
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* --- Toast Notifications --- */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 99999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
pointer-events: none;
|
||||
max-width: 420px;
|
||||
}
|
||||
.toast {
|
||||
pointer-events: auto;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-primary);
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
|
||||
font-size: 12px;
|
||||
color: var(--text-primary);
|
||||
animation: toastSlideIn 0.25s ease-out;
|
||||
transition: opacity 0.3s, transform 0.3s;
|
||||
}
|
||||
.toast.removing {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
.toast-icon {
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.toast-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.toast-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.toast-detail {
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
word-break: break-word;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.toast.toast-success {
|
||||
border-color: rgba(34,197,94,0.4);
|
||||
background: linear-gradient(135deg, rgba(34,197,94,0.08), var(--bg-card));
|
||||
}
|
||||
.toast.toast-success .toast-icon { color: #22c55e; }
|
||||
.toast.toast-error {
|
||||
border-color: rgba(239,68,68,0.4);
|
||||
background: linear-gradient(135deg, rgba(239,68,68,0.08), var(--bg-card));
|
||||
}
|
||||
.toast.toast-error .toast-icon { color: #ef4444; }
|
||||
.toast.toast-warning {
|
||||
border-color: rgba(251,191,36,0.4);
|
||||
background: linear-gradient(135deg, rgba(251,191,36,0.08), var(--bg-card));
|
||||
}
|
||||
.toast.toast-warning .toast-icon { color: #fbbf24; }
|
||||
.toast.toast-info {
|
||||
border-color: rgba(34,211,238,0.4);
|
||||
background: linear-gradient(135deg, rgba(34,211,238,0.08), var(--bg-card));
|
||||
}
|
||||
.toast.toast-info .toast-icon { color: #22d3ee; }
|
||||
@keyframes toastSlideIn {
|
||||
from { opacity: 0; transform: translateX(30px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
@@ -3,6 +3,33 @@
|
||||
* Frontend logic for the tracing/analysis interface
|
||||
*/
|
||||
|
||||
// --- Toast Notification System ---
|
||||
function showToast(type, title, detail, duration) {
|
||||
// type: 'success' | 'error' | 'warning' | 'info'
|
||||
let container = document.getElementById('toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.id = 'toast-container';
|
||||
container.className = 'toast-container';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
const icons = { success: '\u2705', error: '\u274C', warning: '\u26A0\uFE0F', info: '\u2139\uFE0F' };
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.innerHTML = `
|
||||
<span class="toast-icon">${icons[type] || icons.info}</span>
|
||||
<div class="toast-body">
|
||||
<div class="toast-title">${title}</div>
|
||||
${detail ? `<div class="toast-detail">${detail}</div>` : ''}
|
||||
</div>`;
|
||||
container.appendChild(toast);
|
||||
const autoDismiss = duration || (type === 'error' ? 8000 : 4000);
|
||||
setTimeout(() => {
|
||||
toast.classList.add('removing');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, autoDismiss);
|
||||
}
|
||||
|
||||
// --- State ---
|
||||
let state = {
|
||||
alerts: [],
|
||||
@@ -281,7 +308,7 @@ function renderDashboard() {
|
||||
<div class="service-card-actions">
|
||||
<button class="btn btn-sm" onclick="event.stopPropagation(); openRustinelDetail()">Details</button>
|
||||
<button class="btn btn-sm" onclick="event.stopPropagation(); switchTab('tracing')">Trace Console</button>
|
||||
${!rOnline ? '<button class="btn btn-sm btn-launch" onclick="event.stopPropagation(); launchService(\'rustinel\')">Launch</button>' : ''}
|
||||
${!rOnline ? '<button class="btn btn-sm btn-launch" onclick="event.stopPropagation(); launchService(\'rustinel\', this)">Launch</button>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
@@ -305,7 +332,7 @@ function renderDashboard() {
|
||||
<div class="service-card-actions">
|
||||
<button class="btn btn-sm" onclick="event.stopPropagation(); openAgentDetail()">Details</button>
|
||||
<button class="btn btn-sm" onclick="event.stopPropagation(); switchTab('submit')">Submit Sample</button>
|
||||
${!aOnline ? '<button class="btn btn-sm btn-launch" onclick="event.stopPropagation(); launchService(\'detonator_agent\')">Launch</button>' : ''}
|
||||
${!aOnline ? '<button class="btn btn-sm btn-launch" onclick="event.stopPropagation(); launchService(\'detonator_agent\', this)">Launch</button>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
@@ -328,7 +355,7 @@ function renderDashboard() {
|
||||
<div class="service-card-actions">
|
||||
<button class="btn btn-sm" onclick="event.stopPropagation(); openLitterboxDetail()">Details</button>
|
||||
<button class="btn btn-sm" onclick="event.stopPropagation(); window.open('http://localhost:1337', '_blank')">Open UI</button>
|
||||
${!lOnline ? '<button class="btn btn-sm btn-launch" onclick="event.stopPropagation(); launchService(\'litterbox\')">Launch</button>' : ''}
|
||||
${!lOnline ? '<button class="btn btn-sm btn-launch" onclick="event.stopPropagation(); launchService(\'litterbox\', this)">Launch</button>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
@@ -350,7 +377,7 @@ function renderDashboard() {
|
||||
</div>
|
||||
<div class="service-card-actions">
|
||||
<button class="btn btn-sm" onclick="event.stopPropagation(); switchTab('sysmon'); refreshSysmon();">View Events</button>
|
||||
${!sOnline ? '<button class="btn btn-sm btn-launch" onclick="event.stopPropagation(); launchService(\'sysmon\')">Launch</button>' : ''}
|
||||
${!sOnline ? '<button class="btn btn-sm btn-launch" onclick="event.stopPropagation(); launchService(\'sysmon\', this)">Launch</button>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
@@ -370,7 +397,7 @@ function renderDashboard() {
|
||||
<div class="service-metric"><div class="service-metric-value">Kernel</div><div class="service-metric-label">LEVEL</div></div>
|
||||
<div class="service-metric"><div class="service-metric-value">v3.0</div><div class="service-metric-label">VERSION</div></div>
|
||||
</div>
|
||||
${!fOnline ? '<div class="service-card-actions"><button class="btn btn-sm btn-launch" onclick="event.stopPropagation(); launchService(\'fibratus\')">Launch</button></div>' : ''}
|
||||
${!fOnline ? '<div class="service-card-actions"><button class="btn btn-sm btn-launch" onclick="event.stopPropagation(); launchService(\'fibratus\', this)">Launch</button></div>' : ''}
|
||||
</div>
|
||||
`);
|
||||
|
||||
@@ -2954,12 +2981,15 @@ function setStatus(elementId, online) {
|
||||
}
|
||||
}
|
||||
|
||||
async function launchService(serviceName) {
|
||||
const btn = event.currentTarget;
|
||||
async function launchService(serviceName, btnElement) {
|
||||
const btn = btnElement || (typeof event !== 'undefined' && event ? event.currentTarget : null);
|
||||
if (!btn) { console.error('launchService: no button reference'); return; }
|
||||
const originalText = btn.textContent;
|
||||
const displayName = serviceName.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
||||
btn.textContent = 'Starting...';
|
||||
btn.disabled = true;
|
||||
btn.classList.add('launching');
|
||||
showToast('info', `Starting ${displayName}...`, 'Sending launch request');
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/service/launch', {
|
||||
@@ -2973,29 +3003,31 @@ async function launchService(serviceName) {
|
||||
btn.textContent = 'Launched';
|
||||
btn.classList.remove('launching');
|
||||
btn.classList.add('launched');
|
||||
showToast('success', `${displayName} launched`, data.message || 'Service started successfully');
|
||||
// Refresh status after a brief delay to let service start
|
||||
setTimeout(() => refreshDashboard(), 3000);
|
||||
} else {
|
||||
btn.textContent = 'Failed';
|
||||
btn.classList.remove('launching');
|
||||
btn.classList.add('launch-failed');
|
||||
console.error('Launch failed:', data.error);
|
||||
const errorDetail = data.error || 'Unknown error';
|
||||
showToast('error', `${displayName} failed to start`, errorDetail);
|
||||
setTimeout(() => {
|
||||
btn.textContent = originalText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('launch-failed');
|
||||
}, 3000);
|
||||
}, 5000);
|
||||
}
|
||||
} catch (e) {
|
||||
btn.textContent = 'Error';
|
||||
btn.classList.remove('launching');
|
||||
btn.classList.add('launch-failed');
|
||||
console.error('Launch error:', e);
|
||||
showToast('error', `${displayName} — connection error`, e.message || 'Could not reach the server');
|
||||
setTimeout(() => {
|
||||
btn.textContent = originalText;
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('launch-failed');
|
||||
}, 3000);
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3154,11 +3186,12 @@ function renderDetonationResults(data, container) {
|
||||
// LitterBox upload stage
|
||||
if (data.litterbox) {
|
||||
const ok = data.litterbox.status >= 200 && data.litterbox.status < 400;
|
||||
const errorDetail = data.litterbox.error ? ` — ${data.litterbox.error}` : '';
|
||||
html += `<div class="det-stage ${ok ? 'ok' : 'fail'}">
|
||||
<div class="det-stage-icon">${ok ? '✅' : '❌'}</div>
|
||||
<div class="det-stage-info">
|
||||
<div class="det-stage-title">LitterBox Upload</div>
|
||||
<div class="det-stage-detail">${ok ? 'Uploaded' : 'Failed'}</div>
|
||||
<div class="det-stage-detail">${ok ? 'Uploaded' : 'Failed (HTTP ' + data.litterbox.status + ')' + escapeHtml(errorDetail)}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
@@ -3166,11 +3199,12 @@ function renderDetonationResults(data, container) {
|
||||
// LitterBox static analysis stage
|
||||
if (data.litterbox_static) {
|
||||
const ok = data.litterbox_static.triggered;
|
||||
html += `<div class="det-stage ${ok ? 'ok' : 'pending'}">
|
||||
<div class="det-stage-icon">${ok ? '✅' : '⏳'}</div>
|
||||
const errorDetail = data.litterbox_static.error ? ` — ${data.litterbox_static.error}` : '';
|
||||
html += `<div class="det-stage ${ok ? 'ok' : 'fail'}">
|
||||
<div class="det-stage-icon">${ok ? '✅' : '❌'}</div>
|
||||
<div class="det-stage-info">
|
||||
<div class="det-stage-title">Static Analysis</div>
|
||||
<div class="det-stage-detail">${ok ? 'Triggered (YARA + CheckPlz + Strings)' : 'Not triggered'}</div>
|
||||
<div class="det-stage-detail">${ok ? 'Triggered (YARA + CheckPlz + Strings)' : 'Not triggered' + escapeHtml(errorDetail)}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
@@ -3178,21 +3212,35 @@ function renderDetonationResults(data, container) {
|
||||
// LitterBox dynamic analysis stage
|
||||
if (data.litterbox_dynamic) {
|
||||
const ok = data.litterbox_dynamic.triggered;
|
||||
html += `<div class="det-stage ${ok ? 'ok' : 'pending'}">
|
||||
<div class="det-stage-icon">${ok ? '✅' : '⏳'}</div>
|
||||
const errorDetail = data.litterbox_dynamic.error ? ` — ${data.litterbox_dynamic.error}` : '';
|
||||
html += `<div class="det-stage ${ok ? 'ok' : 'fail'}">
|
||||
<div class="det-stage-icon">${ok ? '✅' : '❌'}</div>
|
||||
<div class="det-stage-info">
|
||||
<div class="det-stage-title">Dynamic Analysis</div>
|
||||
<div class="det-stage-detail">${ok ? `Triggered (PE-Sieve, Moneta, HollowsHunter) — ${data.litterbox_dynamic.target}` : 'Not triggered'}</div>
|
||||
<div class="det-stage-detail">${ok ? `Triggered (PE-Sieve, Moneta, HollowsHunter) — ${data.litterbox_dynamic.target}` : 'Not triggered' + escapeHtml(errorDetail)}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Fibratus/EDR stage (always pending initially)
|
||||
// Beacon scan stage
|
||||
if (data.beacon_scan) {
|
||||
const ok = data.beacon_scan.triggered;
|
||||
const tools = data.beacon_scan.tools ? data.beacon_scan.tools.join(', ') : '';
|
||||
html += `<div class="det-stage ${ok ? 'ok' : 'pending'}" id="det-beacon-stage">
|
||||
<div class="det-stage-icon">${ok ? '✅' : '⏳'}</div>
|
||||
<div class="det-stage-info">
|
||||
<div class="det-stage-title">Beacon Scanning</div>
|
||||
<div class="det-stage-detail">${ok ? `Triggered (${tools}) — scanning PID for C2 beacons...` : (data.beacon_scan.reason || 'Not triggered')}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Fibratus/EDR stage (always pending initially, status check happens on first poll)
|
||||
html += `<div class="det-stage pending" id="det-fibratus-stage">
|
||||
<div class="det-stage-icon">⏳</div>
|
||||
<div class="det-stage-info">
|
||||
<div class="det-stage-title">Fibratus / Rustinel EDR</div>
|
||||
<div class="det-stage-detail">Waiting for detection alerts...</div>
|
||||
<div class="det-stage-detail">Checking EDR service status...</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
@@ -3233,26 +3281,73 @@ function pollDetonationResults(sha256, pid, lbHash, filename, attempt) {
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
renderDetonationPanels(data);
|
||||
// Update Fibratus stage indicator
|
||||
// Check service status
|
||||
const edrStatus = data.edr_status || {};
|
||||
const fibratusOffline = edrStatus.fibratus_online === false;
|
||||
const rustinelOffline = edrStatus.rustinel_online === false;
|
||||
const bothEdrOffline = fibratusOffline && rustinelOffline;
|
||||
const litterboxOffline = data.litterbox_online === false;
|
||||
|
||||
// Update LitterBox stage indicators if LitterBox is offline
|
||||
if (litterboxOffline && attempt === 0) {
|
||||
// Show warning on static/dynamic stages if they haven't succeeded
|
||||
const stageCards = document.querySelectorAll('.det-stage');
|
||||
stageCards.forEach(card => {
|
||||
const title = card.querySelector('.det-stage-title')?.textContent || '';
|
||||
const detail = card.querySelector('.det-stage-detail');
|
||||
if ((title.includes('Static') || title.includes('Dynamic')) && card.classList.contains('fail')) {
|
||||
if (detail && !detail.textContent.includes('offline')) {
|
||||
detail.textContent += ' — LitterBox offline';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update Fibratus/Rustinel stage indicator based on EDR status
|
||||
const fStage = document.getElementById('det-fibratus-stage');
|
||||
if (fStage && data.fibratus_alert_count > 0) {
|
||||
fStage.className = 'det-stage ok';
|
||||
fStage.querySelector('.det-stage-icon').innerHTML = '✅';
|
||||
fStage.querySelector('.det-stage-detail').textContent = `${data.fibratus_alert_count} alert(s) detected`;
|
||||
if (fStage) {
|
||||
if (data.fibratus_alert_count > 0) {
|
||||
fStage.className = 'det-stage ok';
|
||||
fStage.querySelector('.det-stage-icon').innerHTML = '✅';
|
||||
fStage.querySelector('.det-stage-detail').textContent = `${data.fibratus_alert_count} alert(s) detected`;
|
||||
} else if (bothEdrOffline) {
|
||||
fStage.className = 'det-stage fail';
|
||||
fStage.querySelector('.det-stage-icon').innerHTML = '⚠';
|
||||
fStage.querySelector('.det-stage-detail').textContent = 'Fibratus and Rustinel are offline — no detection possible';
|
||||
} else if (fibratusOffline) {
|
||||
fStage.querySelector('.det-stage-detail').textContent = 'Fibratus offline — waiting for Rustinel alerts...';
|
||||
} else if (rustinelOffline) {
|
||||
fStage.querySelector('.det-stage-detail').textContent = 'Rustinel offline — waiting for Fibratus alerts...';
|
||||
}
|
||||
}
|
||||
// Keep polling until all results are ready (static + dynamic + fibratus)
|
||||
// Minimum 8 attempts (~40s) to allow EDR rules to fire and alert_loader to pick them up
|
||||
// But skip minimum wait if services are offline
|
||||
const staticReady = data.ready && data.ready.static !== false;
|
||||
const dynamicReady = data.ready && data.ready.dynamic !== false;
|
||||
const fibratusReady = data.ready && data.ready.fibratus;
|
||||
const allReady = staticReady && dynamicReady && fibratusReady;
|
||||
if (!allReady || attempt < 8) {
|
||||
const allOffline = bothEdrOffline && litterboxOffline;
|
||||
const minAttempts = allOffline ? 1 : (bothEdrOffline || litterboxOffline) ? 2 : 8;
|
||||
if (!allReady || attempt < minAttempts) {
|
||||
_detonationPollTimer = setTimeout(() => pollDetonationResults(sha256, pid, lbHash, filename, attempt + 1), 5000);
|
||||
} else {
|
||||
// Final update: show polling complete message
|
||||
const panels = document.getElementById('det-results-panels');
|
||||
if (panels && !panels.querySelector('.det-poll-done')) {
|
||||
panels.insertAdjacentHTML('beforeend', '<div class="det-poll-done">Polling complete. All results collected.</div>');
|
||||
let msg;
|
||||
if (allOffline) {
|
||||
msg = '<div class="det-poll-done">Polling complete. All analysis services offline — no results available.</div>';
|
||||
} else if (litterboxOffline && bothEdrOffline) {
|
||||
msg = '<div class="det-poll-done">Polling complete. LitterBox and EDR services offline.</div>';
|
||||
} else if (litterboxOffline) {
|
||||
msg = '<div class="det-poll-done">Polling complete. LitterBox offline — static/dynamic analysis unavailable.</div>';
|
||||
} else if (bothEdrOffline) {
|
||||
msg = '<div class="det-poll-done">Polling complete. EDR services offline — no detection alerts available.</div>';
|
||||
} else {
|
||||
msg = '<div class="det-poll-done">Polling complete. All results collected.</div>';
|
||||
}
|
||||
panels.insertAdjacentHTML('beforeend', msg);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -3402,6 +3497,90 @@ function renderDetonationPanels(data) {
|
||||
html += `</div></div>`;
|
||||
}
|
||||
|
||||
// --- Beacon Scan Results ---
|
||||
const hasBeaconResults = data.hunt_sleeping_beacons || data.beaconeye;
|
||||
if (hasBeaconResults) {
|
||||
html += `<div class="det-panel">
|
||||
<div class="det-panel-title">BEACON SCANNING</div>
|
||||
<div class="det-panel-body">`;
|
||||
|
||||
// Hunt-Sleeping-Beacons results
|
||||
if (data.hunt_sleeping_beacons) {
|
||||
const hsb = data.hunt_sleeping_beacons;
|
||||
html += `<div class="det-subsection"><span class="det-sub-label">Hunt-Sleeping-Beacons:</span>`;
|
||||
if (hsb.error) {
|
||||
html += `<span class="det-sub-value det-warn">${escapeHtml(hsb.error)}</span>`;
|
||||
} else {
|
||||
const count = hsb.suspicious_count || 0;
|
||||
html += `<span class="det-sub-value ${count > 0 ? 'det-warn' : ''}">`;
|
||||
html += count > 0 ? `${count} suspicious indicator(s) found` : 'No sleeping beacons detected';
|
||||
html += `</span>`;
|
||||
if (hsb.findings && hsb.findings.length > 0) {
|
||||
html += `<div class="det-beacon-findings">`;
|
||||
hsb.findings.slice(0, 10).forEach(f => {
|
||||
const process = f.process || '';
|
||||
const indicators = (f.indicators || []).join('; ');
|
||||
html += `<div class="det-beacon-finding">`;
|
||||
if (process) html += `<span class="det-beacon-proc">${escapeHtml(process)}</span>`;
|
||||
if (indicators) html += `<span class="det-beacon-indicators">${escapeHtml(indicators)}</span>`;
|
||||
html += `</div>`;
|
||||
});
|
||||
html += `</div>`;
|
||||
}
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
// BeaconEye results
|
||||
if (data.beaconeye) {
|
||||
const be = data.beaconeye;
|
||||
html += `<div class="det-subsection"><span class="det-sub-label">BeaconEye:</span>`;
|
||||
if (be.error) {
|
||||
html += `<span class="det-sub-value det-warn">${escapeHtml(be.error)}</span>`;
|
||||
} else {
|
||||
const count = be.beacons_found || 0;
|
||||
html += `<span class="det-sub-value ${count > 0 ? 'det-warn' : ''}">`;
|
||||
html += count > 0 ? `${count} CobaltStrike beacon(s) found` : 'No CobaltStrike beacons detected';
|
||||
html += `</span>`;
|
||||
if (be.findings && be.findings.length > 0) {
|
||||
html += `<div class="det-beacon-findings">`;
|
||||
be.findings.slice(0, 5).forEach(f => {
|
||||
html += `<div class="det-beacon-finding">`;
|
||||
html += `<span class="det-beacon-proc">${escapeHtml(f.summary || '')}</span>`;
|
||||
if (f.config && Object.keys(f.config).length > 0) {
|
||||
const cfgStr = Object.entries(f.config).slice(0, 6).map(([k, v]) => `${k}: ${v}`).join(', ');
|
||||
html += `<span class="det-beacon-indicators">${escapeHtml(cfgStr)}</span>`;
|
||||
}
|
||||
html += `</div>`;
|
||||
});
|
||||
html += `</div>`;
|
||||
}
|
||||
}
|
||||
html += `</div>`;
|
||||
}
|
||||
|
||||
html += `</div></div>`;
|
||||
}
|
||||
|
||||
// Update beacon stage card if results arrived
|
||||
if (hasBeaconResults) {
|
||||
const bStage = document.getElementById('det-beacon-stage');
|
||||
if (bStage) {
|
||||
const hsbCount = data.hunt_sleeping_beacons?.suspicious_count || 0;
|
||||
const beCount = data.beaconeye?.beacons_found || 0;
|
||||
const totalFindings = hsbCount + beCount;
|
||||
if (totalFindings > 0) {
|
||||
bStage.className = 'det-stage ok';
|
||||
bStage.querySelector('.det-stage-icon').innerHTML = '⚠';
|
||||
bStage.querySelector('.det-stage-detail').textContent = `${totalFindings} beacon indicator(s) found`;
|
||||
} else {
|
||||
bStage.className = 'det-stage ok';
|
||||
bStage.querySelector('.det-stage-icon').innerHTML = '✅';
|
||||
bStage.querySelector('.det-stage-detail').textContent = 'Scan complete — no beacons detected';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show polling status if nothing yet
|
||||
if (!html) {
|
||||
html = `<div class="det-panel-loading"><div class="loading-spinner"></div><span>Waiting for results... Analysis may take 1-3 minutes.</span></div>`;
|
||||
@@ -3496,9 +3675,9 @@ const graphState = {
|
||||
};
|
||||
|
||||
async function graphRefresh() {
|
||||
// Fetch process tree + sysmon network/DNS data in parallel
|
||||
// Fetch process tree (limited to top 200 by threats) + sysmon network/DNS data in parallel
|
||||
const [procResp, sysmonNetResp, sysmonDnsResp, sysmonInjectResp] = await Promise.all([
|
||||
fetch('/api/processes'),
|
||||
fetch('/api/processes?max=200&sort=threats&include_parents=true'),
|
||||
fetch('/api/sysmon?event_id=3&max=300'),
|
||||
fetch('/api/sysmon?event_id=22&max=200'),
|
||||
fetch('/api/sysmon?event_id=8&max=100'),
|
||||
@@ -3839,6 +4018,30 @@ function buildGraph(processes, networkEvents, dnsEvents, injectEvents, networkAl
|
||||
applyForceLayout(nodes, edges);
|
||||
}
|
||||
|
||||
// Safety cap: if still too many nodes after filtering, truncate to prevent browser hang
|
||||
const MAX_RENDER_NODES = 500;
|
||||
if (nodes.length > MAX_RENDER_NODES) {
|
||||
// Keep process nodes first (sorted by threats desc), then auxiliary nodes
|
||||
const procNodes = nodes.filter(n => n.type === 'process').sort((a, b) => (b.threats || 0) - (a.threats || 0));
|
||||
const otherNodes = nodes.filter(n => n.type !== 'process');
|
||||
const kept = procNodes.slice(0, MAX_RENDER_NODES);
|
||||
const keptIds = new Set(kept.map(n => n.id));
|
||||
// Keep auxiliary nodes connected to kept processes
|
||||
const keptOther = otherNodes.filter(n => {
|
||||
const edge = edges.find(e => e.source === n.id || e.target === n.id);
|
||||
if (!edge) return false;
|
||||
const otherId = edge.source === n.id ? edge.target : edge.source;
|
||||
return keptIds.has(otherId);
|
||||
});
|
||||
nodes.length = 0;
|
||||
nodes.push(...kept, ...keptOther.slice(0, 200));
|
||||
// Filter edges to only reference existing nodes
|
||||
const allNodeIds = new Set(nodes.map(n => n.id));
|
||||
const validEdges = edges.filter(e => allNodeIds.has(e.source) && allNodeIds.has(e.target));
|
||||
edges.length = 0;
|
||||
edges.push(...validEdges);
|
||||
}
|
||||
|
||||
graphState.nodes = nodes;
|
||||
graphState.edges = edges;
|
||||
|
||||
@@ -4256,7 +4459,7 @@ function renderGraph() {
|
||||
ctx.font = '14px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
const showDetonatedOnly = document.getElementById('graph-filter-detonated')?.checked;
|
||||
const showDetonatedOnly = document.getElementById('graph-show-detonated')?.checked;
|
||||
const msg = showDetonatedOnly
|
||||
? 'No detonated processes found. Submit a sample to see detonation activity.'
|
||||
: 'No process data available.';
|
||||
@@ -4746,12 +4949,31 @@ async function refreshSysmon() {
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Sysmon fetch error:', e);
|
||||
const container = document.getElementById('sysmon-stats');
|
||||
if (container) {
|
||||
container.innerHTML = `<div class="sysmon-diagnostic warning">Connection error: ${escapeHtml(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderSysmonStats() {
|
||||
const container = document.getElementById('sysmon-stats');
|
||||
if (!container || !sysmonStats || !sysmonStats.stats) return;
|
||||
if (!container || !sysmonStats) return;
|
||||
|
||||
// Show diagnostic message if present (log doesn't exist, is empty, etc.)
|
||||
if (sysmonStats.diagnostic) {
|
||||
const level = sysmonStats.online ? 'info' : 'warning';
|
||||
container.innerHTML = `<div class="sysmon-diagnostic ${level}">${escapeHtml(sysmonStats.diagnostic)}</div>`;
|
||||
return;
|
||||
}
|
||||
if (sysmonStats.error) {
|
||||
container.innerHTML = `<div class="sysmon-diagnostic error">Error: ${escapeHtml(sysmonStats.error)}</div>`;
|
||||
return;
|
||||
}
|
||||
if (!sysmonStats.stats || !sysmonStats.stats.length) {
|
||||
container.innerHTML = `<div class="sysmon-diagnostic info">No event statistics available.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = sysmonStats.stats;
|
||||
const total = stats.reduce((sum, s) => sum + s.count, 0);
|
||||
|
||||
@@ -1,4 +1,88 @@
|
||||
[
|
||||
{
|
||||
"id": "5e9f52bac286",
|
||||
"timestamp": "2026-06-13T22:36:51.977366",
|
||||
"filename": "mimikatz.exe",
|
||||
"sha256": "61c0810a23580cf492a6ba4f7654566108331e7a4134c968c2d6a05261b2d8a1",
|
||||
"size": 1355264,
|
||||
"target": "both",
|
||||
"agent_status": "success",
|
||||
"agent_pid": null,
|
||||
"litterbox_status": "success",
|
||||
"file_path": "C:\\Users\\vagrant\\Desktop\\infected\\mimikatz.exe"
|
||||
},
|
||||
{
|
||||
"id": "be457f2a0bec",
|
||||
"timestamp": "2026-06-13T22:35:00.066005",
|
||||
"filename": "mimikatz.exe",
|
||||
"sha256": "61c0810a23580cf492a6ba4f7654566108331e7a4134c968c2d6a05261b2d8a1",
|
||||
"size": 1355264,
|
||||
"target": "both",
|
||||
"agent_status": "success",
|
||||
"agent_pid": null,
|
||||
"litterbox_status": "success",
|
||||
"file_path": "C:\\Users\\vagrant\\Desktop\\infected\\mimikatz.exe"
|
||||
},
|
||||
{
|
||||
"id": "4503e22d428b",
|
||||
"timestamp": "2026-06-13T22:34:51.679575",
|
||||
"filename": "mimikatz.exe",
|
||||
"sha256": "61c0810a23580cf492a6ba4f7654566108331e7a4134c968c2d6a05261b2d8a1",
|
||||
"size": 1355264,
|
||||
"target": "agent",
|
||||
"agent_status": "success",
|
||||
"agent_pid": null,
|
||||
"litterbox_status": null,
|
||||
"file_path": "C:\\Users\\vagrant\\Desktop\\infected\\mimikatz.exe"
|
||||
},
|
||||
{
|
||||
"id": "807ca8abab38",
|
||||
"timestamp": "2026-06-13T22:34:36.319055",
|
||||
"filename": "mimikatz.exe",
|
||||
"sha256": "61c0810a23580cf492a6ba4f7654566108331e7a4134c968c2d6a05261b2d8a1",
|
||||
"size": 1355264,
|
||||
"target": "agent",
|
||||
"agent_status": "success",
|
||||
"agent_pid": null,
|
||||
"litterbox_status": null,
|
||||
"file_path": "C:\\Users\\vagrant\\Desktop\\infected\\mimikatz.exe"
|
||||
},
|
||||
{
|
||||
"id": "d2c0ceca65d9",
|
||||
"timestamp": "2026-06-13T21:34:58.698660",
|
||||
"filename": "mimikatz.exe",
|
||||
"sha256": "61c0810a23580cf492a6ba4f7654566108331e7a4134c968c2d6a05261b2d8a1",
|
||||
"size": 1355264,
|
||||
"target": "both",
|
||||
"agent_status": "success",
|
||||
"agent_pid": 18612,
|
||||
"litterbox_status": "success",
|
||||
"file_path": "C:\\Users\\vagrant\\Desktop\\infected\\mimikatz.exe"
|
||||
},
|
||||
{
|
||||
"id": "72934009f8d9",
|
||||
"timestamp": "2026-06-13T21:09:06.447960",
|
||||
"filename": "npp.8.9.6.2.Installer.x64.exe",
|
||||
"sha256": "7c243203265ce8fdac76c839bf744ae35dcf620760eb97c2ea279af498560e45",
|
||||
"size": 6898288,
|
||||
"target": "both",
|
||||
"agent_status": "failed",
|
||||
"agent_pid": null,
|
||||
"litterbox_status": "success",
|
||||
"file_path": "C:\\Users\\vagrant\\Desktop\\infected\\npp.8.9.6.2.Installer.x64.exe"
|
||||
},
|
||||
{
|
||||
"id": "d237ca316ffc",
|
||||
"timestamp": "2026-06-13T20:50:31.444823",
|
||||
"filename": "npp.8.9.6.2.Installer.x64.exe",
|
||||
"sha256": "7c243203265ce8fdac76c839bf744ae35dcf620760eb97c2ea279af498560e45",
|
||||
"size": 6898288,
|
||||
"target": "both",
|
||||
"agent_status": "failed",
|
||||
"agent_pid": null,
|
||||
"litterbox_status": "success",
|
||||
"file_path": "C:\\Users\\vagrant\\Desktop\\infected\\npp.8.9.6.2.Installer.x64.exe"
|
||||
},
|
||||
{
|
||||
"id": "66187199fb12",
|
||||
"timestamp": "2026-06-10T11:21:06.849116",
|
||||
|
||||