diff --git a/Vagrantfile b/Vagrantfile index 1a1177e..59f9279 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -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", diff --git a/Vagrantfile.utm b/Vagrantfile.utm index ae41a59..cd18d27 100644 --- a/Vagrantfile.utm +++ b/Vagrantfile.utm @@ -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", diff --git a/dev.ps1 b/dev.ps1 new file mode 100644 index 0000000..caa61ef --- /dev/null +++ b/dev.ps1 @@ -0,0 +1,102 @@ +<# +.SYNOPSIS + Transportable Detonation Chamber - Frontend Development Mode + +.DESCRIPTION + Starts the Web UI in development mode with: + - Live-reload for CSS/JS/HTML changes (auto-refreshes browser) + - Flask debug mode (auto-restarts on Python changes) + - Auto-opens browser on startup + - Optional mock mode for offline development + +.EXAMPLE + .\dev.ps1 Start dev server (default: port 9000) + .\dev.ps1 -Mock Start with mock backend services + .\dev.ps1 -Port 8080 Start on a custom port + .\dev.ps1 -NoOpen Don't auto-open the browser +#> + +param( + [Parameter()] + [int]$Port = 9000, + + [Parameter()] + [switch]$Mock, + + [Parameter()] + [switch]$NoOpen, + + [Parameter()] + [string]$Host = "127.0.0.1" +) + +$ErrorActionPreference = 'Stop' +$VenvDir = "webui\.venv" +$PyExe = "$VenvDir\Scripts\python.exe" + +# --- Preflight Checks --- + +# Check venv exists +if (-not (Test-Path $PyExe)) { + Write-Host "" + Write-Host " [ERROR] Python venv not found." -ForegroundColor Red + Write-Host " Run '.\make.ps1 install' first to set up the environment." -ForegroundColor Yellow + Write-Host "" + exit 1 +} + +# Verify watchdog is installed (needed for live-reload) +$watchdogCheck = & $PyExe -c "import watchdog; print('ok')" 2>$null +if ($watchdogCheck -ne 'ok') { + Write-Host "[dev] Installing missing dependency: watchdog..." -ForegroundColor Yellow + & $PyExe -m pip install watchdog -q +} + +# --- Banner --- +Write-Host "" +Write-Host " ================================================================" -ForegroundColor DarkCyan +Write-Host " Transportable Detonation Chamber" -ForegroundColor Cyan -NoNewline +Write-Host " - Dev Mode" -ForegroundColor Yellow +Write-Host " ================================================================" -ForegroundColor DarkCyan +Write-Host "" +Write-Host " Features:" -ForegroundColor White +Write-Host " * Live-reload CSS/JS/HTML changes refresh browser automatically" -ForegroundColor DarkGray +Write-Host " * Debug mode Python changes restart the server" -ForegroundColor DarkGray +Write-Host " * File watcher Console shows file change events" -ForegroundColor DarkGray +if ($Mock) { + Write-Host " * Mock mode Backend services are stubbed" -ForegroundColor DarkGray +} +Write-Host "" + +# --- Build command args --- +$devArgs = @("webui\dev_server.py", "--port", $Port, "--host", $Host) + +if ($Mock) { + $devArgs += "--mock" +} + +if ($NoOpen) { + $devArgs += "--no-open" +} + +# --- Start Dev Server --- +Write-Host " Starting dev server..." -ForegroundColor Cyan +Write-Host " URL: http://${Host}:${Port}" -ForegroundColor Green +Write-Host " Stop: Ctrl+C" -ForegroundColor DarkGray +Write-Host "" + +try { + & $PyExe $devArgs +} catch { + # Ctrl+C is expected + if ($_.Exception.Message -notmatch 'PipelineStoppedException') { + Write-Host "" + Write-Host " [ERROR] Dev server exited with error:" -ForegroundColor Red + Write-Host " $($_.Exception.Message)" -ForegroundColor Red + Write-Host "" + } +} finally { + Write-Host "" + Write-Host " Dev server stopped." -ForegroundColor Yellow + Write-Host "" +} diff --git a/make.ps1 b/make.ps1 index 160b544..3bdfd92 100644 --- a/make.ps1 +++ b/make.ps1 @@ -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) { diff --git a/playbooks/malware_found_on_endpoint.yml b/playbooks/malware_found_on_endpoint.yml new file mode 100644 index 0000000..059538b --- /dev/null +++ b/playbooks/malware_found_on_endpoint.yml @@ -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 "" + # 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 "" + # 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 @() } | + 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 "" -Enabled True + New-NetFirewallRule -DisplayName "IR-Allow-Responder-Out" -Direction Outbound -Action Allow -RemoteAddress "" -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 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 | 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 = "" + $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 -Force + # Kill process tree + taskkill /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 "" /Disable + # Remove Run key entry (backup first) + $key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" + $val = Get-ItemProperty $key -Name "" + $val | Out-File C:\IR\Evidence\removed_runkey.txt + Remove-ItemProperty $key -Name "" + # Disable malicious service + Set-Service -Name "" -StartupType Disabled + Stop-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 "" "$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 /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 -Name " + - location: "Services" + check_command: 'Get-WmiObject Win32_Service | Where-Object { $_.PathName -notmatch "Windows|Microsoft|Program Files" }' + remediation: "sc.exe delete " + - 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 " + - 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 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 -Force" + kill_tree: "taskkill /PID /T /F" + block_outbound: "New-NetFirewallRule -DisplayName 'Block' -Direction Outbound -Action Block" + disable_task: "schtasks /Change /TN /Disable" + disable_service: "Set-Service -Name -StartupType Disabled; Stop-Service -Force" + + analysis: + file_hash: "Get-FileHash -Algorithm SHA256 -Path " + signature_check: "Get-AuthenticodeSignature -FilePath " + strings_search: 'Select-String -Path -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 " + check_alerts: ".\make.ps1 alerts" + view_services: ".\make.ps1 services" + view_logs: ".\make.ps1 logs" diff --git a/scripts/configure-services.ps1 b/scripts/configure-services.ps1 index a40de95..b234cde 100644 --- a/scripts/configure-services.ps1 +++ b/scripts/configure-services.ps1 @@ -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 --- diff --git a/scripts/install-beaconeye.ps1 b/scripts/install-beaconeye.ps1 new file mode 100644 index 0000000..fc0353a --- /dev/null +++ b/scripts/install-beaconeye.ps1 @@ -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 "" diff --git a/scripts/install-scanner-tools.ps1 b/scripts/install-scanner-tools.ps1 new file mode 100644 index 0000000..649e88c --- /dev/null +++ b/scripts/install-scanner-tools.ps1 @@ -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 "" diff --git a/static/dashboard.png b/static/dashboard.png new file mode 100644 index 0000000..5a0767e Binary files /dev/null and b/static/dashboard.png differ diff --git a/static/hex_editor.png b/static/hex_editor.png new file mode 100644 index 0000000..6289815 Binary files /dev/null and b/static/hex_editor.png differ diff --git a/static/mimikatz_detonation.png b/static/mimikatz_detonation.png new file mode 100644 index 0000000..d461586 Binary files /dev/null and b/static/mimikatz_detonation.png differ diff --git a/static/pe_analyzer_text_header_section.png b/static/pe_analyzer_text_header_section.png new file mode 100644 index 0000000..2ed547f Binary files /dev/null and b/static/pe_analyzer_text_header_section.png differ diff --git a/static/pe_header_analyzer.png b/static/pe_header_analyzer.png new file mode 100644 index 0000000..ab2a3b3 Binary files /dev/null and b/static/pe_header_analyzer.png differ diff --git a/static/rustinel_analysis.png b/static/rustinel_analysis.png new file mode 100644 index 0000000..40d397a Binary files /dev/null and b/static/rustinel_analysis.png differ diff --git a/static/rustinel_analysis_details.png b/static/rustinel_analysis_details.png new file mode 100644 index 0000000..d54a164 Binary files /dev/null and b/static/rustinel_analysis_details.png differ diff --git a/webui/app.py b/webui/app.py index 41e5c15..0e3d796 100644 --- a/webui/app.py +++ b/webui/app.py @@ -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 = { diff --git a/webui/dev_server.py b/webui/dev_server.py new file mode 100644 index 0000000..7807fa5 --- /dev/null +++ b/webui/dev_server.py @@ -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 = """ + +""" + + +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 "" in data: + data = data.replace("", f"{LIVERELOAD_JS}") + 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() diff --git a/webui/static/css/style.css b/webui/static/css/style.css index 4a61ef9..7f71ccd 100644 --- a/webui/static/css/style.css +++ b/webui/static/css/style.css @@ -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); } +} diff --git a/webui/static/js/app.js b/webui/static/js/app.js index 750af06..45832ea 100644 --- a/webui/static/js/app.js +++ b/webui/static/js/app.js @@ -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 = ` + ${icons[type] || icons.info} +
+
${title}
+ ${detail ? `
${detail}
` : ''} +
`; + 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() {
- ${!rOnline ? '' : ''} + ${!rOnline ? '' : ''}
`); @@ -305,7 +332,7 @@ function renderDashboard() {
- ${!aOnline ? '' : ''} + ${!aOnline ? '' : ''}
`); @@ -328,7 +355,7 @@ function renderDashboard() {
- ${!lOnline ? '' : ''} + ${!lOnline ? '' : ''}
`); @@ -350,7 +377,7 @@ function renderDashboard() {
- ${!sOnline ? '' : ''} + ${!sOnline ? '' : ''}
`); @@ -370,7 +397,7 @@ function renderDashboard() {
Kernel
LEVEL
v3.0
VERSION
- ${!fOnline ? '
' : ''} + ${!fOnline ? '
' : ''} `); @@ -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 += `
${ok ? '✅' : '❌'}
LitterBox Upload
-
${ok ? 'Uploaded' : 'Failed'}
+
${ok ? 'Uploaded' : 'Failed (HTTP ' + data.litterbox.status + ')' + escapeHtml(errorDetail)}
`; } @@ -3166,11 +3199,12 @@ function renderDetonationResults(data, container) { // LitterBox static analysis stage if (data.litterbox_static) { const ok = data.litterbox_static.triggered; - html += `
-
${ok ? '✅' : '⏳'}
+ const errorDetail = data.litterbox_static.error ? ` — ${data.litterbox_static.error}` : ''; + html += `
+
${ok ? '✅' : '❌'}
Static Analysis
-
${ok ? 'Triggered (YARA + CheckPlz + Strings)' : 'Not triggered'}
+
${ok ? 'Triggered (YARA + CheckPlz + Strings)' : 'Not triggered' + escapeHtml(errorDetail)}
`; } @@ -3178,21 +3212,35 @@ function renderDetonationResults(data, container) { // LitterBox dynamic analysis stage if (data.litterbox_dynamic) { const ok = data.litterbox_dynamic.triggered; - html += `
-
${ok ? '✅' : '⏳'}
+ const errorDetail = data.litterbox_dynamic.error ? ` — ${data.litterbox_dynamic.error}` : ''; + html += `
+
${ok ? '✅' : '❌'}
Dynamic Analysis
-
${ok ? `Triggered (PE-Sieve, Moneta, HollowsHunter) — ${data.litterbox_dynamic.target}` : 'Not triggered'}
+
${ok ? `Triggered (PE-Sieve, Moneta, HollowsHunter) — ${data.litterbox_dynamic.target}` : 'Not triggered' + escapeHtml(errorDetail)}
`; } - // 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 += `
+
${ok ? '✅' : '⏳'}
+
+
Beacon Scanning
+
${ok ? `Triggered (${tools}) — scanning PID for C2 beacons...` : (data.beacon_scan.reason || 'Not triggered')}
+
+
`; + } + + // Fibratus/EDR stage (always pending initially, status check happens on first poll) html += `
⏳
Fibratus / Rustinel EDR
-
Waiting for detection alerts...
+
Checking EDR service status...
`; @@ -3233,26 +3281,73 @@ function pollDetonationResults(sha256, pid, lbHash, filename, attempt) { .then(r => r.json()) .then(data => { renderDetonationPanels(data); - // Update Fibratus stage indicator + // Check service status + const edrStatus = data.edr_status || {}; + const fibratusOffline = edrStatus.fibratus_online === false; + const rustinelOffline = edrStatus.rustinel_online === false; + const bothEdrOffline = fibratusOffline && rustinelOffline; + const litterboxOffline = data.litterbox_online === false; + + // Update LitterBox stage indicators if LitterBox is offline + if (litterboxOffline && attempt === 0) { + // Show warning on static/dynamic stages if they haven't succeeded + const stageCards = document.querySelectorAll('.det-stage'); + stageCards.forEach(card => { + const title = card.querySelector('.det-stage-title')?.textContent || ''; + const detail = card.querySelector('.det-stage-detail'); + if ((title.includes('Static') || title.includes('Dynamic')) && card.classList.contains('fail')) { + if (detail && !detail.textContent.includes('offline')) { + detail.textContent += ' — LitterBox offline'; + } + } + }); + } + + // Update Fibratus/Rustinel stage indicator based on EDR status const fStage = document.getElementById('det-fibratus-stage'); - if (fStage && data.fibratus_alert_count > 0) { - fStage.className = 'det-stage ok'; - fStage.querySelector('.det-stage-icon').innerHTML = '✅'; - fStage.querySelector('.det-stage-detail').textContent = `${data.fibratus_alert_count} alert(s) detected`; + if (fStage) { + if (data.fibratus_alert_count > 0) { + fStage.className = 'det-stage ok'; + fStage.querySelector('.det-stage-icon').innerHTML = '✅'; + fStage.querySelector('.det-stage-detail').textContent = `${data.fibratus_alert_count} alert(s) detected`; + } else if (bothEdrOffline) { + fStage.className = 'det-stage fail'; + fStage.querySelector('.det-stage-icon').innerHTML = '⚠'; + fStage.querySelector('.det-stage-detail').textContent = 'Fibratus and Rustinel are offline — no detection possible'; + } else if (fibratusOffline) { + fStage.querySelector('.det-stage-detail').textContent = 'Fibratus offline — waiting for Rustinel alerts...'; + } else if (rustinelOffline) { + fStage.querySelector('.det-stage-detail').textContent = 'Rustinel offline — waiting for Fibratus alerts...'; + } } // Keep polling until all results are ready (static + dynamic + fibratus) // Minimum 8 attempts (~40s) to allow EDR rules to fire and alert_loader to pick them up + // But skip minimum wait if services are offline const staticReady = data.ready && data.ready.static !== false; const dynamicReady = data.ready && data.ready.dynamic !== false; const fibratusReady = data.ready && data.ready.fibratus; const allReady = staticReady && dynamicReady && fibratusReady; - if (!allReady || attempt < 8) { + const allOffline = bothEdrOffline && litterboxOffline; + const minAttempts = allOffline ? 1 : (bothEdrOffline || litterboxOffline) ? 2 : 8; + if (!allReady || attempt < minAttempts) { _detonationPollTimer = setTimeout(() => pollDetonationResults(sha256, pid, lbHash, filename, attempt + 1), 5000); } else { // Final update: show polling complete message const panels = document.getElementById('det-results-panels'); if (panels && !panels.querySelector('.det-poll-done')) { - panels.insertAdjacentHTML('beforeend', '
Polling complete. All results collected.
'); + let msg; + if (allOffline) { + msg = '
Polling complete. All analysis services offline — no results available.
'; + } else if (litterboxOffline && bothEdrOffline) { + msg = '
Polling complete. LitterBox and EDR services offline.
'; + } else if (litterboxOffline) { + msg = '
Polling complete. LitterBox offline — static/dynamic analysis unavailable.
'; + } else if (bothEdrOffline) { + msg = '
Polling complete. EDR services offline — no detection alerts available.
'; + } else { + msg = '
Polling complete. All results collected.
'; + } + panels.insertAdjacentHTML('beforeend', msg); } } }) @@ -3402,6 +3497,90 @@ function renderDetonationPanels(data) { html += `
`; } + // --- Beacon Scan Results --- + const hasBeaconResults = data.hunt_sleeping_beacons || data.beaconeye; + if (hasBeaconResults) { + html += `
+
BEACON SCANNING
+
`; + + // Hunt-Sleeping-Beacons results + if (data.hunt_sleeping_beacons) { + const hsb = data.hunt_sleeping_beacons; + html += `
Hunt-Sleeping-Beacons:`; + if (hsb.error) { + html += `${escapeHtml(hsb.error)}`; + } else { + const count = hsb.suspicious_count || 0; + html += ``; + html += count > 0 ? `${count} suspicious indicator(s) found` : 'No sleeping beacons detected'; + html += ``; + if (hsb.findings && hsb.findings.length > 0) { + html += `
`; + hsb.findings.slice(0, 10).forEach(f => { + const process = f.process || ''; + const indicators = (f.indicators || []).join('; '); + html += `
`; + if (process) html += `${escapeHtml(process)}`; + if (indicators) html += `${escapeHtml(indicators)}`; + html += `
`; + }); + html += `
`; + } + } + html += `
`; + } + + // BeaconEye results + if (data.beaconeye) { + const be = data.beaconeye; + html += `
BeaconEye:`; + if (be.error) { + html += `${escapeHtml(be.error)}`; + } else { + const count = be.beacons_found || 0; + html += ``; + html += count > 0 ? `${count} CobaltStrike beacon(s) found` : 'No CobaltStrike beacons detected'; + html += ``; + if (be.findings && be.findings.length > 0) { + html += `
`; + be.findings.slice(0, 5).forEach(f => { + html += `
`; + html += `${escapeHtml(f.summary || '')}`; + 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 += `${escapeHtml(cfgStr)}`; + } + html += `
`; + }); + html += `
`; + } + } + html += `
`; + } + + html += `
`; + } + + // Update beacon stage card if results arrived + if (hasBeaconResults) { + const bStage = document.getElementById('det-beacon-stage'); + if (bStage) { + const hsbCount = data.hunt_sleeping_beacons?.suspicious_count || 0; + const beCount = data.beaconeye?.beacons_found || 0; + const totalFindings = hsbCount + beCount; + if (totalFindings > 0) { + bStage.className = 'det-stage ok'; + bStage.querySelector('.det-stage-icon').innerHTML = '⚠'; + bStage.querySelector('.det-stage-detail').textContent = `${totalFindings} beacon indicator(s) found`; + } else { + bStage.className = 'det-stage ok'; + bStage.querySelector('.det-stage-icon').innerHTML = '✅'; + bStage.querySelector('.det-stage-detail').textContent = 'Scan complete — no beacons detected'; + } + } + } + // Show polling status if nothing yet if (!html) { html = `
Waiting for results... Analysis may take 1-3 minutes.
`; @@ -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 = `
Connection error: ${escapeHtml(e.message)}
`; + } } } 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 = `
${escapeHtml(sysmonStats.diagnostic)}
`; + return; + } + if (sysmonStats.error) { + container.innerHTML = `
Error: ${escapeHtml(sysmonStats.error)}
`; + return; + } + if (!sysmonStats.stats || !sysmonStats.stats.length) { + container.innerHTML = `
No event statistics available.
`; + return; + } const stats = sysmonStats.stats; const total = stats.reduce((sum, s) => sum + s.count, 0); diff --git a/webui/submissions.json b/webui/submissions.json index c0e9129..1db836d 100644 --- a/webui/submissions.json +++ b/webui/submissions.json @@ -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",