Reworked a little and some screenshots

This commit is contained in:
Der Benji
2026-06-13 22:59:29 +02:00
parent 73cf212d37
commit d035aae74f
20 changed files with 2553 additions and 66 deletions
Vendored
+10
View File
@@ -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",
+10
View File
@@ -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",
+102
View File
@@ -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 ""
}
+7 -1
View File
@@ -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) {
+740
View File
@@ -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"
+61 -4
View File
@@ -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 ---
+146
View File
@@ -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 ""
+202
View File
@@ -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 ""
Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

+486 -30
View File
@@ -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 = {
+323
View File
@@ -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()
+129
View File
@@ -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); }
}
+253 -31
View File
@@ -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 ? '&#x2705;' : '&#x274C;'}</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 ? '&#x2705;' : '&#x23F3;'}</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 ? '&#x2705;' : '&#x274C;'}</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 ? '&#x2705;' : '&#x23F3;'}</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 ? '&#x2705;' : '&#x274C;'}</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 ? '&#x2705;' : '&#x23F3;'}</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">&#x23F3;</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 = '&#x2705;';
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 = '&#x2705;';
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 = '&#x26A0;';
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 = '&#x26A0;';
bStage.querySelector('.det-stage-detail').textContent = `${totalFindings} beacon indicator(s) found`;
} else {
bStage.className = 'det-stage ok';
bStage.querySelector('.det-stage-icon').innerHTML = '&#x2705;';
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);
+84
View File
@@ -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",