Provisioning

This commit is contained in:
Der Benji
2026-06-10 13:47:16 +02:00
parent d134bcc8bb
commit 30c47d8507
8 changed files with 2059 additions and 59 deletions
+16 -5
View File
@@ -77,7 +77,7 @@ A single-page dark-themed interface that aggregates telemetry from all engines:
| **Graph** | Process relationship graph with 5 layouts (Force/Hierarchical/Radial/Circular/Grid), zoom, search |
| **Sysmon** | Windows Sysmon event log viewer (process, network, file, registry, DNS events) |
| **Scanner** | ThreatCheck + DefenderCheck integration (Defender/AMSI engines), scan history |
| **Hex Editor** | Binary viewer with data inspector, drag-and-drop upload, PE Analysis button |
| **Hex Editor** | Binary viewer with data inspector, drag-and-drop upload, PE/ELF Analysis buttons |
| **Submit** | Multi-target detonation (DetonatorAgent + LitterBox), stage-by-stage progress |
### Detection Engines
@@ -92,10 +92,12 @@ A single-page dark-themed interface that aggregates telemetry from all engines:
### Analysis Capabilities
- **PE Header Analysis**: DOS/File/Optional headers, section table with entropy bars, ASLR/DEP/SEH/CFG detection
- **ELF Binary Analysis**: ELF32/64 header, program headers (segments), section table, dynamic linking, symbol imports/exports
- **ELF Security Audit**: PIE, NX stack, RELRO (Full/Partial), stack canary, Fortify, stripped detection
- **Suspicious Import Detection**: Categorized (injection, evasion, credential access, networking, crypto, shellcode)
- **Packer Identification**: UPX, Themida, VMProtect, ASPack, MPRESS, etc. via section name matching
- **RWX Section Flagging**: Read+Write+Execute permissions highlighted
- **TLS Callback Detection**: Anti-debug indicator
- **RWX Section Flagging**: Read+Write+Execute permissions highlighted (PE and ELF)
- **TLS Callback Detection**: Anti-debug indicator (PE)
- **Entropy Visualization**: Per-section Shannon entropy with color coding (red >= 7.0 = packed/encrypted)
### Developer Experience
@@ -197,6 +199,7 @@ Full binary file viewer:
- **Drag-and-drop** file upload or specify VM path
- **Data inspector**: Int8/16/32/64, Float32/64, ASCII, UTF-16 at cursor position
- **PE Analysis**: Button parses full PE structure (headers, sections, imports, entropy, IOC flags)
- **ELF Analysis**: Button parses ELF binaries (header, segments, sections, symbols, security features, suspicious imports)
- **Cross-tab integration**: Scanner "View in Hex" jumps directly to the flagged offset
### Submit
@@ -364,6 +367,13 @@ make submit FILE=./samples/mimikatz.exe TARGET=both
3. Click **PE Analysis** button
4. Review: headers, security features (ASLR/DEP/SEH/CFG), section entropy, suspicious imports, packer indicators
### ELF Analysis
1. Go to **Hex Editor** tab
2. Upload an ELF binary (Linux/BSD executables, shared objects)
3. Click **ELF Analysis** button
4. Review: ELF header, security audit (PIE/NX/RELRO/canary/Fortify), sections, segments, dynamic libraries, suspicious symbol imports
---
## API Reference
@@ -385,15 +395,16 @@ All endpoints served on port `9000`. Responses are JSON.
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/submit` | Submit sample (multipart). Params: `file`, `target` (agent/litterbox/both) |
| GET | `/api/detonation/results` | Poll results. Params: `sha256`, `pid`, `litterbox_hash` |
| GET | `/api/detonation/results` | Poll results. Params: `sha256`, `pid`, `litterbox_hash`, `filename` |
### Hex Editor & PE
### Hex Editor & Binary Analysis
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/file/hex` | Hex dump. Params: `path`, `offset`, `bytes` |
| POST | `/api/file/hex/upload` | Upload file for hex viewing |
| GET | `/api/file/pe` | PE header analysis. Param: `path` |
| GET | `/api/file/elf` | ELF binary analysis. Param: `path` |
### Scanner
Vendored
+5
View File
@@ -62,6 +62,11 @@ Vagrant.configure("2") do |config|
path: "scripts/install-prerequisites.ps1",
privileged: true
config.vm.provision "brave",
type: "shell",
path: "scripts/install-brave.ps1",
privileged: true
config.vm.provision "fibratus",
type: "shell",
path: "scripts/install-fibratus.ps1",
+118
View File
@@ -0,0 +1,118 @@
# install-brave.ps1
# Installs Brave Browser and configures it to open the Detonation Chamber UI on launch
#
# Run as Administrator
$ErrorActionPreference = "Continue"
Set-StrictMode -Version Latest
Write-Host "=== Installing Brave Browser ===" -ForegroundColor Cyan
# --- Install Brave via Chocolatey ---
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
Write-Host "[!] Chocolatey not found - cannot install Brave" -ForegroundColor Red
exit 1
}
$bravePath = "${env:ProgramFiles}\BraveSoftware\Brave-Browser\Application\brave.exe"
if (-not (Test-Path $bravePath)) {
Write-Host "[*] Installing Brave Browser via Chocolatey..." -ForegroundColor Yellow
choco install brave -y --no-progress
# Refresh PATH
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
} else {
Write-Host "[+] Brave Browser already installed" -ForegroundColor Green
}
# Verify installation
$bravePath = "${env:ProgramFiles}\BraveSoftware\Brave-Browser\Application\brave.exe"
if (-not (Test-Path $bravePath)) {
# Try x86 path
$bravePath = "${env:ProgramFiles(x86)}\BraveSoftware\Brave-Browser\Application\brave.exe"
}
if (-not (Test-Path $bravePath)) {
Write-Host "[!] Brave not found after installation - check logs" -ForegroundColor Red
exit 1
}
Write-Host "[+] Brave installed at: $bravePath" -ForegroundColor Green
# --- Configure Brave to open Detonation Chamber UI on startup ---
Write-Host "[*] Configuring Brave startup page..." -ForegroundColor Yellow
$webuiUrl = "http://localhost:9000"
# Set Brave policies via registry (machine-level, works for all users)
$policyPath = "HKLM:\SOFTWARE\Policies\BraveSoftware\Brave"
New-Item -Path $policyPath -Force | Out-Null
# Homepage and startup settings
Set-ItemProperty -Path $policyPath -Name "HomepageLocation" -Value $webuiUrl -Type String
Set-ItemProperty -Path $policyPath -Name "HomepageIsNewTabPage" -Value 0 -Type DWord
Set-ItemProperty -Path $policyPath -Name "RestoreOnStartup" -Value 4 -Type DWord # 4 = Open a list of URLs
# Startup URLs list
$startupUrlsPath = "$policyPath\RestoreOnStartupURLs"
New-Item -Path $startupUrlsPath -Force | Out-Null
Set-ItemProperty -Path $startupUrlsPath -Name "1" -Value $webuiUrl -Type String
# Disable first-run dialogs and welcome page
Set-ItemProperty -Path $policyPath -Name "PromotionalTabsEnabled" -Value 0 -Type DWord
Set-ItemProperty -Path $policyPath -Name "BookmarkBarEnabled" -Value 1 -Type DWord
Write-Host "[+] Brave configured to open $webuiUrl on startup" -ForegroundColor Green
# --- Create Desktop Shortcut ---
Write-Host "[*] Creating desktop shortcut..." -ForegroundColor Yellow
$desktopPath = "C:\Users\vagrant\Desktop"
if (-not (Test-Path $desktopPath)) {
$desktopPath = [Environment]::GetFolderPath("Desktop")
}
$shortcutPath = Join-Path $desktopPath "Detonation Chamber.lnk"
$shell = New-Object -ComObject WScript.Shell
$shortcut = $shell.CreateShortcut($shortcutPath)
$shortcut.TargetPath = $bravePath
$shortcut.Arguments = $webuiUrl
$shortcut.WorkingDirectory = Split-Path $bravePath -Parent
$shortcut.Description = "Detonation Chamber - Unified Web UI"
$shortcut.Save()
Write-Host "[+] Desktop shortcut created: $shortcutPath" -ForegroundColor Green
# --- Set Brave as default browser (best effort) ---
Write-Host "[*] Setting Brave as default HTTP handler..." -ForegroundColor Yellow
# Register Brave ProgId for http/https (requires system-level registry)
$braveProgId = "BraveHTML"
try {
# Set URL associations via registry (may require user consent on newer Windows)
$assocPath = "HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations"
foreach ($proto in @("http", "https")) {
$userChoicePath = "$assocPath\$proto\UserChoice"
# Note: UserChoice is protected on modern Windows, but policies override
New-Item -Path $userChoicePath -Force -ErrorAction SilentlyContinue | Out-Null
Set-ItemProperty -Path $userChoicePath -Name "ProgId" -Value $braveProgId -ErrorAction SilentlyContinue
}
# Also set via policy (more reliable)
$defaultBrowserPath = "HKLM:\SOFTWARE\Policies\BraveSoftware\Brave"
Set-ItemProperty -Path $defaultBrowserPath -Name "DefaultBrowserSettingEnabled" -Value 1 -Type DWord -ErrorAction SilentlyContinue
} catch {
Write-Host "[!] Could not set default browser via registry (Windows may require interactive consent)" -ForegroundColor Yellow
}
# --- Auto-launch Brave on user login ---
Write-Host "[*] Configuring Brave to auto-launch on login..." -ForegroundColor Yellow
$runKeyPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
Set-ItemProperty -Path $runKeyPath -Name "DetonationChamberUI" -Value "`"$bravePath`" $webuiUrl" -Type String
Write-Host "[+] Brave will auto-launch with Detonation Chamber UI on login" -ForegroundColor Green
Write-Host ""
Write-Host "[+] Brave Browser installation complete!" -ForegroundColor Green
Write-Host " Homepage: $webuiUrl" -ForegroundColor Gray
Write-Host " Auto-launch on login: enabled" -ForegroundColor Gray
+815 -30
View File
@@ -442,10 +442,23 @@ def build_process_tree(alerts):
Handles PID reuse: if a PID's executable changes between alerts,
use the most recent process info (latest alert wins).
"""
# Pre-load known detonated PIDs from submission history
_detonated_pids = set()
try:
for sub in _load_submissions():
apid = sub.get("agent_pid")
if apid:
_detonated_pids.add(str(apid))
except Exception:
pass
processes = {}
for alert in alerts:
pid = alert.get("pid")
if pid and pid not in processes:
raw_pid = alert.get("pid")
if not raw_pid:
continue
pid = str(raw_pid)
if pid not in processes:
processes[pid] = {
"pid": pid,
"name": alert.get("process_name", "unknown"),
@@ -519,13 +532,15 @@ def build_process_tree(alerts):
if engine in ("yara", "ioc"):
proc_entry["activity"]["artifacts"] += 1
# Track detonation enrichment (Fibratus / LitterBox)
if alert.get("detonated"):
# Track detonation enrichment (Fibratus / LitterBox / submission PIDs)
if alert.get("detonated") or str(pid) in _detonated_pids:
proc_entry["activity"]["detonated"] += 1
proc_entry["detonated"] = True
det_src = alert.get("detonation_source", "")
if det_src and det_src not in proc_entry["detonation_sources"]:
proc_entry["detonation_sources"].append(det_src)
if str(pid) in _detonated_pids and "agent" not in proc_entry["detonation_sources"]:
proc_entry["detonation_sources"].append("agent")
# Track last seen timestamp
ts = alert.get("timestamp", "")
@@ -540,10 +555,66 @@ def build_process_tree(alerts):
proc_entry["exit_code"] = _get_nested(raw, "process.exit_code")
# Link children to parents
for pid, proc in processes.items():
stub_parents = {}
for pid, proc in list(processes.items()):
ppid = proc.get("parent_pid")
if ppid and ppid in processes:
processes[ppid]["children"].append(pid)
if ppid is not None:
ppid_str = str(ppid)
if ppid_str == str(pid):
continue # self-reference
if ppid_str in processes:
if pid not in processes[ppid_str]["children"]:
processes[ppid_str]["children"].append(pid)
elif ppid_str not in stub_parents:
# Create a stub parent entry so the graph can show the relationship
stub_parents[ppid_str] = {
"pid": ppid,
"name": proc.get("parent_name", "unknown"),
"image": "",
"command_line": proc.get("parent_command_line", ""),
"user": "",
"parent_pid": None,
"parent_name": "",
"integrity": "",
"working_dir": "",
"first_seen": proc.get("first_seen", ""),
"last_seen": proc.get("last_seen", ""),
"exit_time": None,
"exit_code": None,
"children": [pid],
"activity": {
"file": 0, "network": 0, "dns": 0, "http": 0,
"registry": 0, "modules": 0, "scripts": 0, "injection": 0,
"wmi": 0, "services": 0, "tasks": 0, "logons": 0,
"artifacts": 0, "threats": 0, "detonated": 0,
},
"alerts": [],
"detonated": False,
"detonation_sources": [],
"is_stub": True,
}
else:
# Stub parent already created by a sibling; just add this PID as child
if pid not in stub_parents[ppid_str]["children"]:
stub_parents[ppid_str]["children"].append(pid)
# Merge stub parents into processes dict
processes.update(stub_parents)
# Cross-reference with submission history: mark processes whose PID matches
# a known detonated sample (agent_pid) as detonated, including child processes
# (reuse the _detonated_pids set pre-loaded at the top of this function)
for pid in _detonated_pids:
if pid in processes:
processes[pid]["detonated"] = True
if "agent" not in processes[pid]["detonation_sources"]:
processes[pid]["detonation_sources"].append("agent")
# Mark direct children as detonated too (spawned by detonated sample)
for child_pid in processes[pid].get("children", []):
if child_pid in processes:
processes[child_pid]["detonated"] = True
if "child_of_detonated" not in processes[child_pid]["detonation_sources"]:
processes[child_pid]["detonation_sources"].append("child_of_detonated")
# Infer exit for processes whose last_seen is significantly before session end
# (heuristic: if no new events for this process after others continue, mark as exited)
@@ -612,28 +683,34 @@ def api_status():
"""Get status of all integrated services."""
status = {}
# Check DetonatorAgent
# Check DetonatorAgent - validate response is actually valid JSON with 200
try:
r = requests.get(f"{DETONATOR_AGENT_API}/api/lock/status", timeout=1)
status["detonator_agent"] = {"online": True, "data": r.json()}
r = requests.get(f"{DETONATOR_AGENT_API}/api/lock/status", timeout=2)
if r.status_code == 200:
status["detonator_agent"] = {"online": True, "data": r.json()}
else:
status["detonator_agent"] = {"online": False, "status_code": r.status_code}
except Exception:
status["detonator_agent"] = {"online": False}
# Check Detonator (skip if known offline to save time)
# Check Detonator - validate 200 status
try:
r = requests.get(f"{DETONATOR_API}/api/submissions", timeout=1)
status["detonator"] = {"online": True}
r = requests.get(f"{DETONATOR_API}/api/submissions", timeout=2)
status["detonator"] = {"online": r.status_code == 200}
except Exception:
status["detonator"] = {"online": False}
# Check LitterBox
# Check LitterBox - validate 200 status
try:
r = requests.get(LITTERBOX_API, timeout=1)
r = requests.get(LITTERBOX_API, timeout=2)
status["litterbox"] = {"online": r.status_code == 200}
except Exception:
status["litterbox"] = {"online": False}
# Rustinel - check process existence via filesystem (fast)
# Check Fibratus - independent check via service or API
status["fibratus"] = {"online": _is_fibratus_running()}
# Rustinel - check process existence
rustinel_online = os.path.isdir(RUSTINEL_ALERTS_DIR) and _is_rustinel_running()
status["rustinel"] = {
"online": rustinel_online,
@@ -646,14 +723,141 @@ def api_status():
return jsonify(status)
# --- Service launch configuration ---
# Paths are auto-detected: VM paths first, then dev/local paths
def _find_service_launch_config():
"""Detect available service launch commands based on installed paths."""
configs = {}
# Rustinel
for exe in [r"C:\tools\rustinel\rustinel.exe", os.path.join(RUSTINEL_INSTALL_DIR, "rustinel.exe")]:
if os.path.isfile(exe):
configs["rustinel"] = {"exe": exe, "args": "run", "cwd": os.path.dirname(exe)}
break
# DetonatorAgent
for exe in [r"C:\DetonatorAgent\publish\DetonatorAgent.exe"]:
if os.path.isfile(exe):
configs["detonator_agent"] = {"exe": exe, "args": "--port 8080 --edr fibratus", "cwd": os.path.dirname(exe)}
break
# Detonator API
for venv_py in [r"C:\detonator\.venv\Scripts\python.exe"]:
if os.path.isfile(venv_py):
configs["detonator"] = {
"exe": venv_py,
"args": '-c "from detonatorapi.fastapi_app import app; import uvicorn; uvicorn.run(app, host=\'0.0.0.0\', port=8000)"',
"cwd": r"C:\detonator",
}
break
# LitterBox - check multiple possible locations
litterbox_dirs = [
r"C:\LitterBox",
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "LitterBox"),
]
for lb_dir in litterbox_dirs:
lb_script = os.path.join(lb_dir, "litterbox.py")
if os.path.isfile(lb_script):
# Prefer venv python, fallback to system
venv_py = os.path.join(lb_dir, "venv", "Scripts", "python.exe")
py_exe = venv_py if os.path.isfile(venv_py) else "python"
configs["litterbox"] = {"exe": py_exe, "args": "litterbox.py", "cwd": lb_dir}
break
# Fibratus - Windows Service
configs["fibratus"] = {"service": "fibratus"}
# Sysmon - Windows Service
configs["sysmon"] = {"service": "Sysmon64"}
return configs
@app.route("/api/service/launch", methods=["POST"])
def api_service_launch():
"""Launch a service by name."""
data = request.get_json(force=True) if request.is_json else request.form
service_name = data.get("service", "").strip()
if not service_name:
return jsonify({"error": "No service specified"}), 400
configs = _find_service_launch_config()
if service_name not in configs:
return jsonify({"error": f"Unknown service: {service_name}", "available": list(configs.keys())}), 404
config = configs[service_name]
try:
# Windows Service start
if "service" in config:
svc_name = config["service"]
result = subprocess.run(
["powershell", "-NoProfile", "-Command", f"Start-Service -Name '{svc_name}' -ErrorAction Stop"],
capture_output=True, text=True, timeout=10
)
if result.returncode != 0:
return jsonify({"error": f"Failed to start service: {result.stderr.strip()}"}), 500
return jsonify({"success": True, "message": f"Service '{svc_name}' started"})
# Process launch
exe = config["exe"]
args = config.get("args", "")
cwd = config.get("cwd", "")
if not os.path.isfile(exe) and exe != "python":
return jsonify({"error": f"Executable not found: {exe}"}), 404
# Launch detached process
cmd_parts = [exe] + (args.split() if args and not args.startswith('-c') else ([args] if args else []))
if args.startswith('-c'):
cmd_parts = [exe, "-c", args[3:].strip().strip('"')]
subprocess.Popen(
cmd_parts,
cwd=cwd or None,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
creationflags=0x00000008 | 0x00000200, # DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
)
# Invalidate status caches so next poll picks up new state
_rustinel_proc_cache["checked_at"] = 0
_sysmon_proc_cache["checked_at"] = 0
_fibratus_proc_cache["checked_at"] = 0
return jsonify({"success": True, "message": f"Launched {service_name}"})
except subprocess.TimeoutExpired:
return jsonify({"error": "Launch timed out"}), 500
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/service/configs")
def api_service_configs():
"""Return detected service launch configurations."""
configs = _find_service_launch_config()
# Sanitize for frontend (just report which are launchable)
result = {}
for name, cfg in configs.items():
if "service" in cfg:
result[name] = {"type": "service", "service_name": cfg["service"], "launchable": True}
else:
launchable = os.path.isfile(cfg["exe"]) or cfg["exe"] == "python"
result[name] = {"type": "process", "launchable": launchable, "cwd": cfg.get("cwd", "")}
return jsonify(result)
# Cache for rustinel process check (avoid spawning powershell on every request)
_rustinel_proc_cache = {"online": False, "checked_at": 0}
def _is_rustinel_running():
"""Fast check if Rustinel is running (cached for 10s)."""
"""Fast check if Rustinel is running (cached for 5s)."""
now = time.time()
if now - _rustinel_proc_cache["checked_at"] < 10:
if now - _rustinel_proc_cache["checked_at"] < 5:
return _rustinel_proc_cache["online"]
try:
result = subprocess.run(
@@ -666,8 +870,9 @@ def _is_rustinel_running():
_rustinel_proc_cache["checked_at"] = now
return online
except Exception:
_rustinel_proc_cache["online"] = False
_rustinel_proc_cache["checked_at"] = now
return _rustinel_proc_cache["online"]
return False
def _get_rustinel_process():
@@ -692,9 +897,9 @@ _sysmon_proc_cache = {"online": False, "checked_at": 0}
def _is_sysmon_running():
"""Fast check if Sysmon64 service is running (cached for 30s)."""
"""Fast check if Sysmon64 service is running (cached for 10s)."""
now = time.time()
if now - _sysmon_proc_cache["checked_at"] < 30:
if now - _sysmon_proc_cache["checked_at"] < 10:
return _sysmon_proc_cache["online"]
try:
result = subprocess.run(
@@ -707,8 +912,36 @@ def _is_sysmon_running():
_sysmon_proc_cache["checked_at"] = now
return online
except Exception:
_sysmon_proc_cache["online"] = False
_sysmon_proc_cache["checked_at"] = now
return _sysmon_proc_cache["online"]
return False
# Cache for Fibratus check
_fibratus_proc_cache = {"online": False, "checked_at": 0}
def _is_fibratus_running():
"""Check if Fibratus is running as a service or process (cached for 10s)."""
now = time.time()
if now - _fibratus_proc_cache["checked_at"] < 10:
return _fibratus_proc_cache["online"]
try:
result = subprocess.run(
["powershell", "-NoProfile", "-Command",
"$svc = Get-Service -Name fibratus -ErrorAction SilentlyContinue; "
"if ($svc -and $svc.Status -eq 'Running') { 'True' } "
"else { (Get-Process -Name fibratus -ErrorAction SilentlyContinue) -ne $null }"],
capture_output=True, text=True, timeout=3
)
online = "True" in result.stdout
_fibratus_proc_cache["online"] = online
_fibratus_proc_cache["checked_at"] = now
return online
except Exception:
_fibratus_proc_cache["online"] = False
_fibratus_proc_cache["checked_at"] = now
return False
def _get_rustinel_rules():
@@ -1234,9 +1467,10 @@ def api_submissions():
@app.route("/api/detonation/results")
def api_detonation_results():
"""Poll for combined detonation results: LitterBox static/dynamic + Fibratus/Rustinel alerts.
Query params: sha256 (file hash), pid (agent PID), litterbox_hash (LB hash if different)."""
Query params: sha256 (file hash), pid (agent PID), litterbox_hash (LB hash if different), filename."""
sha256 = request.args.get("sha256", "")
pid = request.args.get("pid", "")
filename = request.args.get("filename", "")
lb_hash = request.args.get("litterbox_hash", "") or sha256
results = {"sha256": sha256, "pid": pid, "ready": {}}
@@ -1293,14 +1527,42 @@ def api_detonation_results():
search_terms.add(sha256[:16]) # Partial match on hash prefix
for alert in events_store.get("alerts", []):
# Match by PID
alert_pid = str(alert.get("process", {}).get("pid", ""))
alert_hash = alert.get("file", {}).get("hash", {}).get("sha256", "") or ""
alert_name = alert.get("process", {}).get("name", "") or ""
# Match by PID (flat field from parse_rustinel_alert / parse_fibratus_alert)
alert_pid = str(alert.get("pid", ""))
alert_ppid = str(alert.get("parent_pid", ""))
# Also check the raw event for nested process.pid (ECS format)
raw = alert.get("raw", {})
raw_pid = str(_get_nested(raw, "process.pid", ""))
raw_ppid = str(_get_nested(raw, "process.parent.pid", ""))
# Check file hash from raw event
alert_hash = _get_nested(raw, "file.hash.sha256", "") or ""
# Also check process.hash.sha256 (some rules attach it there)
proc_hash = _get_nested(raw, "process.hash.sha256", "") or ""
# Check command_line and process_image for sample filename
cmdline = (alert.get("command_line", "") or "").lower()
proc_image = (alert.get("process_image", "") or "").lower()
proc_name = (alert.get("process_name", "") or "").lower()
if pid and alert_pid == str(pid):
matching_alerts.append(alert)
elif sha256 and sha256.lower() in alert_hash.lower():
matched = False
if pid:
pid_str = str(pid)
if alert_pid == pid_str or raw_pid == pid_str or alert_ppid == pid_str or raw_ppid == pid_str:
matched = True
if not matched and sha256 and len(sha256) >= 16:
sha_lower = sha256.lower()
if (sha_lower[:16] in alert_hash.lower() or
sha_lower[:16] in proc_hash.lower()):
matched = True
if not matched and filename and len(filename) >= 3:
# Match by sample filename in command line, process image, or process name
fn_lower = filename.lower()
# Strip extension for broader matching (e.g. "mimikatz" matches "mimikatz.exe")
fn_stem = fn_lower.rsplit(".", 1)[0] if "." in fn_lower else fn_lower
if len(fn_stem) >= 3 and (fn_lower in cmdline or fn_lower in proc_image or
fn_stem in proc_name or fn_stem in cmdline):
matched = True
if matched:
matching_alerts.append(alert)
results["fibratus_alerts"] = matching_alerts[:50]
@@ -1795,6 +2057,7 @@ def api_file_pe():
"virtual_size": section.Misc_VirtualSize,
"raw_size": section.SizeOfRawData,
"raw_offset": hex(section.PointerToRawData),
"raw_offset_dec": section.PointerToRawData,
"characteristics": hex(section.Characteristics),
"executable": bool(section.Characteristics & 0x20000000),
"writable": bool(section.Characteristics & 0x80000000),
@@ -2043,6 +2306,528 @@ def api_file_pe_section():
return jsonify(result)
# --- ELF Analysis ---
# Suspicious ELF symbols/function imports (similar to PE suspicious imports)
SUSPICIOUS_ELF_IMPORTS = {
"process_injection": ["ptrace", "process_vm_writev", "process_vm_readv", "__libc_dlopen_mode"],
"code_execution": ["mprotect", "mmap", "execve", "execvp", "execl", "system", "popen", "dlopen", "dlsym"],
"anti_debug": ["ptrace", "prctl", "getppid", "kill"],
"networking": ["socket", "connect", "bind", "listen", "accept", "send", "recv", "sendto", "recvfrom", "getaddrinfo"],
"file_operations": ["unlink", "rename", "chmod", "chown", "fchmod", "link", "symlink", "mount"],
"privilege_escalation": ["setuid", "setgid", "seteuid", "setreuid", "setregid", "capset"],
"crypto": ["EVP_EncryptInit", "EVP_DecryptInit", "AES_encrypt", "AES_decrypt", "RSA_public_encrypt", "RC4"],
"evasion": ["fork", "daemon", "setsid", "dup2", "memfd_create", "fexecve"],
}
# Known ELF section names indicating packers/protectors
ELF_PACKER_SECTIONS = {
"upx": "UPX",
".upx": "UPX",
"UPX!": "UPX",
".themida": "Themida",
".enigma": "Enigma Protector",
".vmprotect": "VMProtect",
".packed": "Generic Packer",
".crypted": "Encrypted/Packed",
}
@app.route("/api/file/elf")
def api_file_elf():
"""Parse ELF header and return structured analysis with IOC indicators."""
filepath = request.args.get("path", "")
if not filepath:
return jsonify({"error": "No path specified"}), 400
norm_path = os.path.normpath(filepath)
if not os.path.isfile(norm_path):
return jsonify({"error": "File not found"}), 404
try:
with open(norm_path, "rb") as f:
data = f.read()
except Exception as e:
return jsonify({"error": f"Cannot read file: {e}"}), 500
# Verify ELF magic
if len(data) < 64 or data[:4] != b"\x7fELF":
return jsonify({"error": "Not a valid ELF file (missing \\x7fELF magic)"}), 400
result = {"valid": True, "path": filepath, "file_size": len(data)}
# --- ELF Identification (e_ident) ---
ei_class = data[4] # 1=32bit, 2=64bit
ei_data = data[5] # 1=little-endian, 2=big-endian
ei_version = data[6]
ei_osabi = data[7]
is_64 = ei_class == 2
is_le = ei_data == 1
endian = "<" if is_le else ">"
class_map = {1: "ELF32", 2: "ELF64"}
data_map = {1: "Little-endian (LSB)", 2: "Big-endian (MSB)"}
osabi_map = {
0: "UNIX System V", 1: "HP-UX", 2: "NetBSD", 3: "Linux",
6: "Solaris", 7: "AIX", 8: "IRIX", 9: "FreeBSD",
10: "Tru64", 11: "Novell Modesto", 12: "OpenBSD",
64: "ARM EABI", 97: "ARM", 255: "Standalone"
}
result["ident"] = {
"class": class_map.get(ei_class, f"Unknown ({ei_class})"),
"is_64bit": is_64,
"data": data_map.get(ei_data, f"Unknown ({ei_data})"),
"is_little_endian": is_le,
"version": ei_version,
"osabi": osabi_map.get(ei_osabi, f"Unknown ({ei_osabi})"),
"osabi_raw": ei_osabi,
}
# --- ELF Header ---
type_map = {0: "NONE", 1: "REL (Relocatable)", 2: "EXEC (Executable)", 3: "DYN (Shared Object/PIE)", 4: "CORE"}
machine_map = {
0: "None", 2: "SPARC", 3: "x86 (i386)", 6: "Intel 80486",
8: "MIPS", 20: "PowerPC", 21: "PowerPC64", 22: "S390",
40: "ARM", 43: "SPARC V9", 50: "IA-64", 62: "x86-64 (AMD64)",
183: "AArch64 (ARM64)", 243: "RISC-V", 247: "eBPF",
}
if is_64:
if len(data) < 64:
return jsonify({"error": "File too small for ELF64 header"}), 400
# ELF64 header: e_type(2) e_machine(2) e_version(4) e_entry(8) e_phoff(8) e_shoff(8) e_flags(4) e_ehsize(2) e_phentsize(2) e_phnum(2) e_shentsize(2) e_shnum(2) e_shstrndx(2)
hdr = struct.unpack(f"{endian}HHI QQQ I HHHHHH", data[16:64])
e_type, e_machine, e_version, e_entry, e_phoff, e_shoff, e_flags, e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx = hdr
else:
if len(data) < 52:
return jsonify({"error": "File too small for ELF32 header"}), 400
# ELF32 header
hdr = struct.unpack(f"{endian}HHI III I HHHHHH", data[16:52])
e_type, e_machine, e_version, e_entry, e_phoff, e_shoff, e_flags, e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx = hdr
result["header"] = {
"type": type_map.get(e_type, f"Unknown ({e_type})"),
"type_raw": e_type,
"machine": machine_map.get(e_machine, f"Unknown ({e_machine})"),
"machine_raw": e_machine,
"version": e_version,
"entry_point": hex(e_entry),
"program_header_offset": e_phoff,
"section_header_offset": e_shoff,
"flags": hex(e_flags),
"header_size": e_ehsize,
"ph_entry_size": e_phentsize,
"ph_count": e_phnum,
"sh_entry_size": e_shentsize,
"sh_count": e_shnum,
"sh_str_index": e_shstrndx,
"is_executable": e_type == 2,
"is_shared_object": e_type == 3,
"is_pie": e_type == 3, # DYN with entry point often means PIE
"is_relocatable": e_type == 1,
}
# --- Security Features ---
has_pie = e_type == 3
has_nx = False # Will check PT_GNU_STACK
has_relro = False
has_full_relro = False
has_stack_canary = False # Will check symbols
has_fortify = False # Will check symbols
is_stripped = True # Assume stripped unless we find .symtab
# --- Section Headers ---
sections = []
shstrtab_data = b""
# Read section header string table first
if e_shstrndx < e_shnum and e_shoff > 0:
if is_64:
str_sec_offset = e_shoff + e_shstrndx * e_shentsize
if str_sec_offset + e_shentsize <= len(data):
sh_entry = struct.unpack(f"{endian}IIQQQQIIQQ", data[str_sec_offset:str_sec_offset + 64])
shstrtab_offset = sh_entry[4] # sh_offset
shstrtab_size = sh_entry[5] # sh_size
if shstrtab_offset + shstrtab_size <= len(data):
shstrtab_data = data[shstrtab_offset:shstrtab_offset + shstrtab_size]
else:
str_sec_offset = e_shoff + e_shstrndx * e_shentsize
if str_sec_offset + e_shentsize <= len(data):
sh_entry = struct.unpack(f"{endian}IIIIIIIIII", data[str_sec_offset:str_sec_offset + 40])
shstrtab_offset = sh_entry[4]
shstrtab_size = sh_entry[5]
if shstrtab_offset + shstrtab_size <= len(data):
shstrtab_data = data[shstrtab_offset:shstrtab_offset + shstrtab_size]
def get_shstr(offset):
"""Get null-terminated string from section header string table."""
if offset >= len(shstrtab_data):
return ""
end = shstrtab_data.find(b"\x00", offset)
if end == -1:
end = min(offset + 64, len(shstrtab_data))
return shstrtab_data[offset:end].decode("utf-8", errors="replace")
# Section type map
sh_type_map = {
0: "NULL", 1: "PROGBITS", 2: "SYMTAB", 3: "STRTAB", 4: "RELA",
5: "HASH", 6: "DYNAMIC", 7: "NOTE", 8: "NOBITS", 9: "REL",
10: "SHLIB", 11: "DYNSYM", 14: "INIT_ARRAY", 15: "FINI_ARRAY",
0x6ffffff6: "GNU_HASH", 0x6ffffffd: "VERDEF", 0x6ffffffe: "VERNEED",
0x6fffffff: "VERSYM",
}
total_entropy = calculate_entropy(data)
result["total_entropy"] = total_entropy
for i in range(e_shnum):
offset = e_shoff + i * e_shentsize
if offset + e_shentsize > len(data):
break
if is_64:
sh = struct.unpack(f"{endian}IIQQQQIIQQ", data[offset:offset + 64])
sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size, sh_link, sh_info, sh_addralign, sh_entsize = sh
else:
sh = struct.unpack(f"{endian}IIIIIIIIII", data[offset:offset + 40])
sh_name, sh_type, sh_flags, sh_addr, sh_offset, sh_size, sh_link, sh_info, sh_addralign, sh_entsize = sh
sec_name = get_shstr(sh_name)
# Calculate entropy for this section
sec_entropy = 0.0
if sh_type != 8 and sh_size > 0 and sh_offset + sh_size <= len(data): # Not NOBITS
sec_data_bytes = data[sh_offset:sh_offset + sh_size]
sec_entropy = calculate_entropy(sec_data_bytes)
sec_info = {
"index": i,
"name": sec_name,
"type": sh_type_map.get(sh_type, f"0x{sh_type:x}"),
"type_raw": sh_type,
"flags": sh_flags,
"flags_str": _elf_section_flags_str(sh_flags),
"address": hex(sh_addr),
"offset": hex(sh_offset),
"offset_dec": sh_offset,
"size": sh_size,
"link": sh_link,
"info": sh_info,
"alignment": sh_addralign,
"entry_size": sh_entsize,
"entropy": sec_entropy,
"entropy_status": "high" if sec_entropy >= ENTROPY_HIGH_THRESHOLD else "warn" if sec_entropy >= ENTROPY_WARN_THRESHOLD else "normal",
"executable": bool(sh_flags & 0x4),
"writable": bool(sh_flags & 0x1),
"allocatable": bool(sh_flags & 0x2),
}
# Check for packer indicators
for packer_name, packer_label in ELF_PACKER_SECTIONS.items():
if sec_name.lower().startswith(packer_name.lower()):
sec_info["packer_indicator"] = packer_label
break
# Flag WX (writable + executable) sections
if sec_info["executable"] and sec_info["writable"]:
sec_info["wx_warning"] = True
# Check if .symtab exists (means not fully stripped)
if sec_name == ".symtab":
is_stripped = False
sections.append(sec_info)
result["sections"] = sections
result["is_stripped"] = is_stripped
# --- Program Headers (Segments) ---
segments = []
pt_type_map = {
0: "NULL", 1: "LOAD", 2: "DYNAMIC", 3: "INTERP", 4: "NOTE",
5: "SHLIB", 6: "PHDR", 7: "TLS",
0x6474e550: "GNU_EH_FRAME", 0x6474e551: "GNU_STACK",
0x6474e552: "GNU_RELRO", 0x6474e553: "GNU_PROPERTY",
}
for i in range(e_phnum):
offset = e_phoff + i * e_phentsize
if offset + e_phentsize > len(data):
break
if is_64:
ph = struct.unpack(f"{endian}IIQQQQQQ", data[offset:offset + 56])
p_type, p_flags, p_offset, p_vaddr, p_paddr, p_filesz, p_memsz, p_align = ph
else:
ph = struct.unpack(f"{endian}IIIIIIII", data[offset:offset + 32])
p_type, p_offset, p_vaddr, p_paddr, p_filesz, p_memsz, p_flags, p_align = ph
seg_info = {
"index": i,
"type": pt_type_map.get(p_type, f"0x{p_type:x}"),
"type_raw": p_type,
"flags": p_flags,
"flags_str": _elf_segment_flags_str(p_flags),
"offset": hex(p_offset),
"vaddr": hex(p_vaddr),
"paddr": hex(p_paddr),
"filesz": p_filesz,
"memsz": p_memsz,
"align": p_align,
"readable": bool(p_flags & 4),
"writable": bool(p_flags & 2),
"executable": bool(p_flags & 1),
}
# Check security features
if p_type == 0x6474e551: # GNU_STACK
if not (p_flags & 1): # Not executable
has_nx = True
seg_info["security_note"] = "NX stack" if not (p_flags & 1) else "Executable stack (no NX!)"
if p_type == 0x6474e552: # GNU_RELRO
has_relro = True
seg_info["security_note"] = "RELRO (read-only relocations)"
# Extract interpreter path
if p_type == 3 and p_filesz > 0 and p_offset + p_filesz <= len(data): # PT_INTERP
interp = data[p_offset:p_offset + p_filesz].rstrip(b"\x00").decode("utf-8", errors="replace")
seg_info["interpreter"] = interp
result["interpreter"] = interp
segments.append(seg_info)
result["segments"] = segments
# --- Dynamic Section (imports, needed libraries) ---
dynamic_entries = []
needed_libs = []
soname = None
rpath = None
runpath = None
dynamic_strtab_offset = 0
dynamic_strtab_size = 0
# Find .dynstr section for resolving dynamic string table
dynstr_data = b""
for sec in sections:
if sec["name"] == ".dynstr" and sec["offset_dec"] + sec["size"] <= len(data):
dynstr_data = data[sec["offset_dec"]:sec["offset_dec"] + sec["size"]]
break
def get_dynstr(offset):
if offset >= len(dynstr_data):
return ""
end = dynstr_data.find(b"\x00", offset)
if end == -1:
end = min(offset + 256, len(dynstr_data))
return dynstr_data[offset:end].decode("utf-8", errors="replace")
# Find and parse .dynamic section
for sec in sections:
if sec["name"] == ".dynamic" and sec["offset_dec"] + sec["size"] <= len(data):
dyn_offset = sec["offset_dec"]
dyn_size = sec["size"]
entry_size = 16 if is_64 else 8
dt_tag_map = {
0: "NULL", 1: "NEEDED", 2: "PLTRELSZ", 3: "PLTGOT", 4: "HASH",
5: "STRTAB", 6: "SYMTAB", 7: "RELA", 8: "RELASZ", 9: "RELAENT",
10: "STRSZ", 11: "SYMENT", 12: "INIT", 13: "FINI", 14: "SONAME",
15: "RPATH", 16: "SYMBOLIC", 17: "REL", 20: "PLTREL", 21: "DEBUG",
23: "JMPREL", 24: "BIND_NOW", 25: "INIT_ARRAY", 26: "FINI_ARRAY",
29: "RUNPATH", 30: "FLAGS", 0x6ffffffb: "FLAGS_1",
0x6ffffff0: "VERSYM", 0x6ffffffe: "VERNEED", 0x6fffffff: "VERNEEDNUM",
}
i = 0
while i < dyn_size:
if is_64:
if dyn_offset + i + 16 > len(data):
break
d_tag, d_val = struct.unpack(f"{endian}qQ", data[dyn_offset + i:dyn_offset + i + 16])
else:
if dyn_offset + i + 8 > len(data):
break
d_tag, d_val = struct.unpack(f"{endian}iI", data[dyn_offset + i:dyn_offset + i + 8])
if d_tag == 0: # DT_NULL
break
tag_name = dt_tag_map.get(d_tag, f"0x{d_tag:x}")
if d_tag == 1: # DT_NEEDED
lib = get_dynstr(d_val)
needed_libs.append(lib)
elif d_tag == 14: # DT_SONAME
soname = get_dynstr(d_val)
elif d_tag == 15: # DT_RPATH
rpath = get_dynstr(d_val)
elif d_tag == 29: # DT_RUNPATH
runpath = get_dynstr(d_val)
elif d_tag == 24: # DT_BIND_NOW
has_full_relro = has_relro # RELRO + BIND_NOW = Full RELRO
elif d_tag == 0x6ffffffb: # DT_FLAGS_1
if d_val & 0x1: # DF_1_NOW
has_full_relro = has_relro
i += entry_size
break
result["needed_libraries"] = needed_libs
result["soname"] = soname
result["rpath"] = rpath
result["runpath"] = runpath
# --- Symbol Analysis (from .dynsym) ---
imported_symbols = []
exported_symbols = []
dynsym_data = b""
dynsym_entsize = 24 if is_64 else 16
for sec in sections:
if sec["name"] == ".dynsym" and sec["offset_dec"] + sec["size"] <= len(data):
dynsym_data = data[sec["offset_dec"]:sec["offset_dec"] + sec["size"]]
break
if dynsym_data:
num_syms = len(dynsym_data) // dynsym_entsize
for i in range(1, min(num_syms, 2000)): # Skip index 0, cap at 2000
sym_offset = i * dynsym_entsize
if is_64:
st_name, st_info, st_other, st_shndx, st_value, st_size = struct.unpack(
f"{endian}IBBHQQ", dynsym_data[sym_offset:sym_offset + 24]
)
else:
st_name, st_value, st_size, st_info, st_other, st_shndx = struct.unpack(
f"{endian}IIIBBH", dynsym_data[sym_offset:sym_offset + 16]
)
sym_name = get_dynstr(st_name)
if not sym_name:
continue
st_bind = st_info >> 4
st_type = st_info & 0xf
sym_entry = {
"name": sym_name,
"bind": ["LOCAL", "GLOBAL", "WEAK"][st_bind] if st_bind < 3 else f"OTHER({st_bind})",
"type": ["NOTYPE", "OBJECT", "FUNC", "SECTION", "FILE"][st_type] if st_type < 5 else f"OTHER({st_type})",
"value": hex(st_value),
"size": st_size,
"shndx": st_shndx,
}
if st_shndx == 0: # UND - imported
imported_symbols.append(sym_entry)
else:
exported_symbols.append(sym_entry)
# Check for stack canary / fortify
if sym_name == "__stack_chk_fail" or sym_name == "__stack_chk_guard":
has_stack_canary = True
if "__fortify" in sym_name.lower() or sym_name.endswith("_chk"):
has_fortify = True
result["imported_symbols"] = imported_symbols[:500]
result["exported_symbols"] = exported_symbols[:500]
result["import_count"] = len(imported_symbols)
result["export_count"] = len(exported_symbols)
# --- Suspicious Import Detection ---
suspicious_found = {}
for sym in imported_symbols:
for category, api_list in SUSPICIOUS_ELF_IMPORTS.items():
if sym["name"] in api_list:
if category not in suspicious_found:
suspicious_found[category] = []
suspicious_found[category].append(sym["name"])
result["suspicious_imports"] = suspicious_found
# --- Security Summary ---
result["security"] = {
"pie": has_pie,
"nx": has_nx,
"relro": "Full" if has_full_relro else ("Partial" if has_relro else "None"),
"stack_canary": has_stack_canary,
"fortify": has_fortify,
"stripped": is_stripped,
}
# --- IOC Flags ---
flags = []
if not has_nx:
flags.append({"type": "no_nx", "severity": "high", "detail": "NX (non-executable stack) not enabled — stack is executable"})
if not has_pie:
flags.append({"type": "no_pie", "severity": "medium", "detail": "Not a position-independent executable (no ASLR for main binary)"})
if not has_relro:
flags.append({"type": "no_relro", "severity": "medium", "detail": "No RELRO — GOT is writable (GOT overwrite attacks possible)"})
elif not has_full_relro:
flags.append({"type": "partial_relro", "severity": "low", "detail": "Partial RELRO — GOT partially protected"})
if not has_stack_canary:
flags.append({"type": "no_canary", "severity": "medium", "detail": "No stack canary detected (__stack_chk_fail not imported)"})
if is_stripped:
flags.append({"type": "stripped", "severity": "low", "detail": "Binary is stripped (no .symtab — harder to analyze)"})
if rpath:
flags.append({"type": "rpath", "severity": "medium", "detail": f"RPATH set: {rpath} (potential DLL hijacking)"})
if runpath:
flags.append({"type": "runpath", "severity": "low", "detail": f"RUNPATH set: {runpath}"})
if suspicious_found:
for cat, syms in suspicious_found.items():
sev = "high" if cat in ("process_injection", "privilege_escalation", "anti_debug") else "medium"
flags.append({"type": "suspicious_import", "category": cat, "severity": sev, "detail": f"{len(syms)} suspicious symbol(s): {', '.join(syms[:5])}"})
for sec in sections:
if sec.get("wx_warning"):
flags.append({"type": "wx_section", "severity": "high", "detail": f"Section '{sec['name']}' is Writable+Executable (W^X violation)"})
if sec["entropy_status"] == "high":
flags.append({"type": "high_entropy", "severity": "medium", "detail": f"Section '{sec['name']}' entropy {sec['entropy']:.2f} (packed/encrypted)"})
if sec.get("packer_indicator"):
flags.append({"type": "packer", "severity": "medium", "detail": f"Section '{sec['name']}' matches packer: {sec['packer_indicator']}"})
if total_entropy >= ENTROPY_HIGH_THRESHOLD:
flags.append({"type": "high_total_entropy", "severity": "medium", "detail": f"Overall file entropy {total_entropy:.2f} suggests packing/encryption"})
result["flags"] = flags
result["flag_count"] = {
"high": len([f for f in flags if f["severity"] == "high"]),
"medium": len([f for f in flags if f["severity"] == "medium"]),
"low": len([f for f in flags if f["severity"] == "low"]),
}
return jsonify(result)
def _elf_section_flags_str(flags):
"""Convert ELF section flags to human-readable string."""
parts = []
if flags & 0x1: parts.append("W")
if flags & 0x2: parts.append("A")
if flags & 0x4: parts.append("X")
if flags & 0x10: parts.append("M")
if flags & 0x20: parts.append("S")
if flags & 0x40: parts.append("I")
if flags & 0x80: parts.append("L")
if flags & 0x100: parts.append("O")
if flags & 0x200: parts.append("G")
if flags & 0x400: parts.append("T")
return "".join(parts) if parts else "—"
def _elf_segment_flags_str(flags):
"""Convert ELF segment flags to human-readable string."""
parts = []
if flags & 4: parts.append("R")
if flags & 2: parts.append("W")
if flags & 1: parts.append("X")
return "".join(parts) if parts else "—"
# --- Main ---
if __name__ == "__main__":
# Start background alert loader
+411 -1
View File
@@ -1622,6 +1622,17 @@ body.hex-resizing {
background: rgba(34,211,238,0.18);
border-color: rgba(34,211,238,0.5);
}
.section-link {
color: var(--accent-cyan);
text-decoration: none;
cursor: pointer;
border-bottom: 1px dotted rgba(34,211,238,0.4);
transition: color 0.15s, border-color 0.15s;
}
.section-link:hover {
color: #67e8f9;
border-bottom-color: rgba(34,211,238,0.8);
}
.pe-sec-detail-row td {
padding: 0 !important;
border-bottom: 1px solid var(--border-primary);
@@ -1774,6 +1785,259 @@ body.hex-resizing {
}
.btn-accent:hover { background: rgba(34,211,238,0.2); }
/* --- ELF Analysis Panel --- */
.elf-panel {
margin-top: 12px;
border: 1px solid var(--border-primary);
border-radius: var(--radius);
background: var(--bg-tertiary);
overflow: hidden;
}
.elf-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: rgba(168,85,247,0.06);
border-bottom: 1px solid var(--border-primary);
}
.elf-panel-header h3 { margin: 0; font-size: 13px; font-weight: 700; color: var(--text-primary); }
.elf-panel-body { padding: 16px; }
.elf-error { color: var(--accent-red); font-size: 12px; padding: 12px; background: rgba(239,68,68,0.05); border: 1px solid rgba(239,68,68,0.2); border-radius: var(--radius); }
.elf-loading { display: flex; align-items: center; gap: 10px; font-size: 12px; color: var(--text-muted); padding: 20px; }
.elf-empty { font-size: 11px; color: var(--text-muted); text-align: center; padding: 20px; }
/* ELF Flags Banner */
.elf-flags-banner {
margin-bottom: 16px;
padding: 12px 14px;
border-radius: var(--radius);
border: 1px solid rgba(239,68,68,0.2);
background: rgba(239,68,68,0.04);
}
.elf-flags-banner.clean {
border-color: rgba(34,197,94,0.2);
background: rgba(34,197,94,0.04);
font-size: 12px;
color: #22c55e;
}
.elf-flags-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text-primary);
margin-bottom: 10px;
}
.elf-flags-icon { font-size: 14px; }
.elf-flag-counts { margin-left: auto; display: flex; gap: 6px; }
.elf-flag-badge {
font-size: 9px;
font-weight: 700;
padding: 2px 6px;
border-radius: 3px;
letter-spacing: 0.5px;
}
.elf-flag-badge.high { background: rgba(239,68,68,0.15); color: #ef4444; }
.elf-flag-badge.med { background: rgba(251,191,36,0.15); color: #fbbf24; }
.elf-flag-badge.low { background: rgba(96,165,250,0.15); color: #60a5fa; }
.elf-flags-list { display: flex; flex-direction: column; gap: 4px; }
.elf-flag-item {
display: flex;
align-items: center;
gap: 10px;
padding: 5px 8px;
font-size: 11px;
border-left: 3px solid transparent;
border-radius: 2px;
}
.elf-flag-item.sev-high { border-left-color: #ef4444; background: rgba(239,68,68,0.03); }
.elf-flag-item.sev-medium { border-left-color: #fbbf24; background: rgba(251,191,36,0.03); }
.elf-flag-item.sev-low { border-left-color: #60a5fa; }
.elf-flag-sev { font-size: 9px; font-weight: 700; min-width: 42px; }
.elf-flag-item.sev-high .elf-flag-sev { color: #ef4444; }
.elf-flag-item.sev-medium .elf-flag-sev { color: #fbbf24; }
.elf-flag-item.sev-low .elf-flag-sev { color: #60a5fa; }
.elf-flag-detail { color: var(--text-primary); }
/* ELF Overview Grid */
.elf-overview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.elf-card {
border: 1px solid var(--border-primary);
border-radius: var(--radius);
padding: 12px;
background: var(--bg-secondary);
}
.elf-card-title {
font-size: 10px;
font-weight: 700;
color: var(--accent-purple);
letter-spacing: 0.8px;
margin-bottom: 10px;
text-transform: uppercase;
}
.elf-field {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
font-size: 11px;
border-bottom: 1px solid var(--border-primary);
}
.elf-field:last-child { border-bottom: none; }
.elf-label { color: var(--text-muted); }
.elf-value { color: var(--text-primary); font-weight: 500; }
.elf-value.elf-ok { color: #22c55e; }
.elf-value.elf-bad { color: #ef4444; }
.elf-value.elf-warn { color: #fbbf24; }
.elf-value.elf-dim { color: var(--text-muted); }
/* ELF Section Blocks */
.elf-section-block { margin-bottom: 16px; }
/* ELF Libraries List */
.elf-libs-list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.elf-lib-badge {
font-size: 10px;
font-family: var(--font-mono);
padding: 3px 8px;
background: rgba(168,85,247,0.08);
border: 1px solid rgba(168,85,247,0.2);
border-radius: 3px;
color: #c084fc;
}
/* ELF Tables */
.elf-table {
width: 100%;
border-collapse: collapse;
font-size: 10px;
margin-top: 8px;
}
.elf-table th {
text-align: left;
padding: 6px 8px;
font-size: 9px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
background: var(--bg-primary);
border-bottom: 1px solid var(--border-primary);
}
.elf-table td {
padding: 5px 8px;
border-bottom: 1px solid var(--border-primary);
color: var(--text-secondary);
}
.elf-table tr.elf-row-high { background: rgba(239,68,68,0.04); }
.elf-table tr.elf-row-high td { color: #fca5a5; }
.elf-table tr.elf-row-warn { background: rgba(251,191,36,0.03); }
/* ELF Entropy Bars */
.elf-entropy-bar {
position: relative;
width: 80px;
height: 10px;
background: var(--bg-primary);
border-radius: 5px;
overflow: hidden;
display: inline-flex;
align-items: center;
}
.elf-entropy-fill { height: 100%; border-radius: 5px; transition: width 0.3s; }
.elf-entropy-fill.normal { background: linear-gradient(90deg, #22c55e, #4ade80); }
.elf-entropy-fill.warn { background: linear-gradient(90deg, #eab308, #fbbf24); }
.elf-entropy-fill.high { background: linear-gradient(90deg, #dc2626, #ef4444); }
.elf-entropy-val { font-size: 9px; font-family: var(--font-mono); margin-left: 6px; min-width: 28px; }
.elf-status-badge {
font-size: 9px;
font-weight: 600;
padding: 2px 5px;
border-radius: 3px;
text-transform: uppercase;
}
.elf-status-badge.high { background: rgba(239,68,68,0.15); color: #ef4444; }
.elf-status-badge.warn { background: rgba(251,191,36,0.15); color: #fbbf24; }
.elf-status-badge.normal { background: rgba(34,197,94,0.1); color: #22c55e; }
/* ELF W+X Badge */
.elf-wx-badge {
font-size: 8px;
font-weight: 700;
background: rgba(239,68,68,0.15);
color: #ef4444;
padding: 1px 4px;
border-radius: 2px;
margin-left: 4px;
vertical-align: middle;
}
.elf-packer-badge {
font-size: 8px;
font-weight: 700;
background: rgba(251,191,36,0.15);
color: #fbbf24;
padding: 1px 4px;
border-radius: 2px;
margin-left: 4px;
vertical-align: middle;
}
/* ELF Segment Notes */
.elf-seg-note {
font-size: 9px;
color: var(--accent-cyan);
}
/* ELF Suspicious Imports */
.elf-suspicious-title { color: #ef4444 !important; }
.elf-suspicious-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 10px;
margin-top: 8px;
}
.elf-suspicious-card {
border: 1px solid rgba(239,68,68,0.2);
border-radius: var(--radius);
padding: 10px;
background: rgba(239,68,68,0.03);
}
.elf-suspicious-cat {
font-size: 10px;
font-weight: 700;
color: #fca5a5;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.elf-suspicious-syms {
font-size: 10px;
color: var(--text-secondary);
line-height: 1.6;
}
.elf-suspicious-syms code {
font-family: var(--font-mono);
color: var(--accent-cyan);
background: rgba(34,211,238,0.06);
padding: 1px 4px;
border-radius: 2px;
}
/* ELF Collapsible Sections */
.elf-collapsible {
cursor: pointer;
user-select: none;
}
.elf-collapsible:hover { color: var(--text-primary); }
.elf-collapse-body { margin-top: 8px; }
.elf-sym-table td { font-size: 10px; }
/* =============================================
EXISTING STYLES (kept)
============================================= */
@@ -2466,6 +2730,33 @@ body.hex-resizing {
font-size: 10px;
}
.btn-launch {
background: rgba(34,197,94,0.1) !important;
color: #22c55e !important;
border-color: rgba(34,197,94,0.3) !important;
font-weight: 600;
}
.btn-launch:hover {
background: rgba(34,197,94,0.2) !important;
border-color: rgba(34,197,94,0.5) !important;
}
.btn-launch.launching {
background: rgba(251,191,36,0.1) !important;
color: #fbbf24 !important;
border-color: rgba(251,191,36,0.3) !important;
cursor: wait;
}
.btn-launch.launched {
background: rgba(34,197,94,0.15) !important;
color: #22c55e !important;
border-color: rgba(34,197,94,0.4) !important;
}
.btn-launch.launch-failed {
background: rgba(239,68,68,0.1) !important;
color: #ef4444 !important;
border-color: rgba(239,68,68,0.3) !important;
}
.service-card-glow {
position: absolute;
top: -50%;
@@ -3218,6 +3509,7 @@ body.hex-resizing {
.det-alert-item.sev-low .det-alert-sev { color: #60a5fa; }
.det-alert-rule { flex: 1; color: var(--text-primary); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.det-alert-proc { color: var(--text-muted); font-size: 10px; }
.det-alert-engine { font-size: 9px; color: var(--accent-purple); background: rgba(168,85,247,0.1); padding: 1px 5px; border-radius: 3px; }
.det-subsection {
display: flex;
align-items: baseline;
@@ -3602,8 +3894,126 @@ body.hex-resizing {
.badge-dim { background: rgba(148,163,184,0.1); color: var(--text-muted); }
.btn-xs { font-size: 9px; padding: 2px 6px; border-radius: 3px; background: var(--bg-tertiary); border: 1px solid var(--border-primary); color: var(--text-secondary); cursor: pointer; }
.btn-xs:hover { background: var(--bg-secondary); color: var(--text-primary); border-color: var(--border-accent); }
.btn-lb-results { background: rgba(167,139,250,0.08); color: #a78bfa; border-color: rgba(167,139,250,0.25); }
.btn-lb-results:hover { background: rgba(167,139,250,0.18); border-color: rgba(167,139,250,0.5); color: #c4b5fd; }
/* --- Scanner Tab --- */
/* --- LitterBox Results Panel --- */
.lb-analyses-list {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 400px;
overflow-y: auto;
}
.lb-analysis-entry {
padding: 8px 10px;
background: var(--bg-primary);
border: 1px solid var(--border-primary);
border-radius: var(--radius);
cursor: pointer;
transition: all 0.15s;
}
.lb-analysis-entry:hover {
border-color: rgba(167,139,250,0.4);
background: rgba(167,139,250,0.03);
}
.lb-entry-main {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.lb-entry-name { font-size: 11px; font-weight: 500; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 200px; }
.lb-entry-time { font-size: 10px; color: var(--text-muted); }
.lb-entry-meta { display: flex; align-items: center; gap: 8px; font-size: 10px; }
.lb-entry-score {
font-weight: 700;
padding: 1px 5px;
border-radius: 3px;
font-size: 9px;
}
.lb-entry-score.high { background: rgba(239,68,68,0.15); color: #ef4444; }
.lb-entry-score.med { background: rgba(251,191,36,0.15); color: #fbbf24; }
.lb-entry-score.low { background: rgba(34,197,94,0.1); color: #22c55e; }
.lb-entry-hash { color: var(--text-dim); }
.lb-entry-status { color: var(--text-muted); }
.lb-result-content { padding: 4px 0; }
.lb-subsection { margin-bottom: 12px; }
.lb-sub-title {
font-size: 10px;
font-weight: 700;
color: var(--text-muted);
letter-spacing: 0.5px;
margin-bottom: 6px;
}
.lb-tag-list { display: flex; flex-wrap: wrap; gap: 4px; }
.lb-yara-tag {
font-size: 10px;
font-family: var(--font-mono);
padding: 2px 7px;
border-radius: 3px;
background: rgba(239,68,68,0.08);
border: 1px solid rgba(239,68,68,0.2);
color: #fca5a5;
}
.lb-detection-tag {
font-size: 10px;
font-family: var(--font-mono);
padding: 2px 7px;
border-radius: 3px;
background: rgba(251,191,36,0.08);
border: 1px solid rgba(251,191,36,0.2);
color: #fbbf24;
}
.lb-meta { font-size: 11px; color: var(--text-secondary); margin-bottom: 6px; }
.lb-strings-list {
max-height: 180px;
overflow-y: auto;
background: var(--bg-primary);
border: 1px solid var(--border-primary);
border-radius: var(--radius);
padding: 6px 0;
}
.lb-string-entry {
font-size: 10px;
padding: 2px 10px;
color: var(--text-primary);
border-bottom: 1px solid rgba(255,255,255,0.02);
word-break: break-all;
}
.lb-dynamic-stats { display: flex; gap: 10px; margin-bottom: 6px; }
.lb-stat {
font-size: 11px;
font-weight: 500;
padding: 3px 8px;
border-radius: 3px;
}
.lb-stat.ok { background: rgba(34,197,94,0.08); color: #22c55e; }
.lb-stat.warn { background: rgba(251,191,36,0.08); color: #fbbf24; }
.lb-stat.bad { background: rgba(239,68,68,0.08); color: #ef4444; }
.lb-raw {
font-family: var(--font-mono);
font-size: 10px;
line-height: 1.5;
color: var(--text-secondary);
background: var(--bg-primary);
border: 1px solid var(--border-primary);
border-radius: var(--radius);
padding: 8px 10px;
max-height: 200px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
margin: 0;
}
.lb-raw-details { margin-top: 6px; }
.lb-raw-details summary {
font-size: 10px;
color: var(--text-muted);
cursor: pointer;
user-select: none;
}
.lb-raw-details summary:hover { color: var(--text-primary); }
.scanner-body {
padding: 16px;
display: flex;
+633 -21
View File
@@ -281,6 +281,7 @@ function renderDashboard() {
<div class="service-card-actions">
<button class="btn btn-sm" onclick="event.stopPropagation(); openRustinelDetail()">Details</button>
<button class="btn btn-sm" onclick="event.stopPropagation(); switchTab('tracing')">Trace Console</button>
${!rOnline ? '<button class="btn btn-sm btn-launch" onclick="event.stopPropagation(); launchService(\'rustinel\')">Launch</button>' : ''}
</div>
</div>
`);
@@ -304,6 +305,7 @@ function renderDashboard() {
<div class="service-card-actions">
<button class="btn btn-sm" onclick="event.stopPropagation(); openAgentDetail()">Details</button>
<button class="btn btn-sm" onclick="event.stopPropagation(); switchTab('submit')">Submit Sample</button>
${!aOnline ? '<button class="btn btn-sm btn-launch" onclick="event.stopPropagation(); launchService(\'detonator_agent\')">Launch</button>' : ''}
</div>
</div>
`);
@@ -326,6 +328,7 @@ function renderDashboard() {
<div class="service-card-actions">
<button class="btn btn-sm" onclick="event.stopPropagation(); openLitterboxDetail()">Details</button>
<button class="btn btn-sm" onclick="event.stopPropagation(); window.open('http://localhost:1337', '_blank')">Open UI</button>
${!lOnline ? '<button class="btn btn-sm btn-launch" onclick="event.stopPropagation(); launchService(\'litterbox\')">Launch</button>' : ''}
</div>
</div>
`);
@@ -347,12 +350,13 @@ function renderDashboard() {
</div>
<div class="service-card-actions">
<button class="btn btn-sm" onclick="event.stopPropagation(); switchTab('sysmon'); refreshSysmon();">View Events</button>
${!sOnline ? '<button class="btn btn-sm btn-launch" onclick="event.stopPropagation(); launchService(\'sysmon\')">Launch</button>' : ''}
</div>
</div>
`);
// Fibratus Card
const fOnline = status.fibratus?.online || rOnline; // assumes running if Rustinel is
const fOnline = status.fibratus?.online || false;
cards.push(`
<div class="service-card fibratus-card ${fOnline ? '' : 'offline'}">
<div class="service-card-glow"></div>
@@ -366,6 +370,7 @@ function renderDashboard() {
<div class="service-metric"><div class="service-metric-value">Kernel</div><div class="service-metric-label">LEVEL</div></div>
<div class="service-metric"><div class="service-metric-value">v3.0</div><div class="service-metric-label">VERSION</div></div>
</div>
${!fOnline ? '<div class="service-card-actions"><button class="btn btn-sm btn-launch" onclick="event.stopPropagation(); launchService(\'fibratus\')">Launch</button></div>' : ''}
</div>
`);
@@ -1726,8 +1731,9 @@ function renderPeAnalysis(pe, container) {
const entropyPct = Math.min(100, (sec.entropy / 8) * 100);
const barColor = sec.entropy_status === 'high' ? '#ef4444' : sec.entropy_status === 'warn' ? '#fbbf24' : '#22c55e';
const secOffsetDec = sec.raw_offset_dec != null ? sec.raw_offset_dec : parseInt(sec.raw_offset, 16);
html += `<tr class="${rowClass}" id="pe-sec-row-${idx}">
<td class="mono">${escapeHtml(sec.name)}${packer}${rwx}</td>
<td class="mono"><a class="section-link" href="#" onclick="peJumpToSection(${secOffsetDec}); return false;" title="View in Hex Editor">${escapeHtml(sec.name)}</a>${packer}${rwx}</td>
<td class="mono">${sec.virtual_address}</td>
<td>${formatSize(sec.virtual_size)}</td>
<td>${formatSize(sec.raw_size)}</td>
@@ -1957,6 +1963,248 @@ function peJumpToSection(rawOffset) {
if (editorBody) editorBody.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// --- ELF Analysis ---
async function elfAnalyze() {
const filepath = state.hexFilePath || document.getElementById('hex-filepath').value.trim();
const panel = document.getElementById('elf-panel');
const body = document.getElementById('elf-panel-body');
// Hide PE panel if open
document.getElementById('pe-panel').style.display = 'none';
if (!filepath) {
panel.style.display = 'block';
body.innerHTML = '<div class="elf-error">No file loaded. Load a file in the hex editor first.</div>';
return;
}
panel.style.display = 'block';
body.innerHTML = '<div class="elf-loading"><div class="loading-spinner"></div><span>Parsing ELF binary...</span></div>';
try {
LoadingSpinner.start();
const resp = await fetch(`/api/file/elf?path=${encodeURIComponent(filepath)}`);
const data = await resp.json();
LoadingSpinner.stop();
if (data.error) {
body.innerHTML = `<div class="elf-error">${escapeHtml(data.error)}</div>`;
return;
}
renderElfAnalysis(data, body);
} catch (e) {
LoadingSpinner.stop();
body.innerHTML = `<div class="elf-error">Failed: ${escapeHtml(e.message)}</div>`;
}
}
function renderElfAnalysis(elf, container) {
let html = '';
// --- IOC Flags Banner ---
if (elf.flags && elf.flags.length > 0) {
html += '<div class="elf-flags-banner">';
html += `<div class="elf-flags-header"><span class="elf-flags-icon">&#x26A0;</span> <strong>${elf.flags.length} IOC Flag${elf.flags.length > 1 ? 's' : ''} Detected</strong>`;
html += `<span class="elf-flag-counts">`;
if (elf.flag_count.high) html += `<span class="elf-flag-badge high">${elf.flag_count.high} HIGH</span>`;
if (elf.flag_count.medium) html += `<span class="elf-flag-badge med">${elf.flag_count.medium} MED</span>`;
if (elf.flag_count.low) html += `<span class="elf-flag-badge low">${elf.flag_count.low} LOW</span>`;
html += `</span></div>`;
html += '<div class="elf-flags-list">';
elf.flags.sort((a, b) => {
const order = {high: 0, medium: 1, low: 2};
return (order[a.severity] || 3) - (order[b.severity] || 3);
}).forEach(f => {
html += `<div class="elf-flag-item sev-${f.severity}"><span class="elf-flag-sev">${f.severity.toUpperCase()}</span><span class="elf-flag-detail">${escapeHtml(f.detail)}</span></div>`;
});
html += '</div></div>';
} else {
html += '<div class="elf-flags-banner clean"><span class="elf-flags-icon">&#x2705;</span> No IOC flags detected.</div>';
}
// --- Overview Grid ---
html += '<div class="elf-overview-grid">';
// ELF Identification card
const ident = elf.ident;
html += `<div class="elf-card">
<div class="elf-card-title">ELF IDENTIFICATION</div>
<div class="elf-field"><span class="elf-label">Class</span><span class="elf-value">${escapeHtml(ident.class)}</span></div>
<div class="elf-field"><span class="elf-label">Data</span><span class="elf-value">${escapeHtml(ident.data)}</span></div>
<div class="elf-field"><span class="elf-label">OS/ABI</span><span class="elf-value">${escapeHtml(ident.osabi)}</span></div>
<div class="elf-field"><span class="elf-label">File Size</span><span class="elf-value">${formatSize(elf.file_size)}</span></div>
<div class="elf-field"><span class="elf-label">Total Entropy</span><span class="elf-value ${elf.total_entropy >= 7.0 ? 'elf-bad' : elf.total_entropy >= 6.5 ? 'elf-warn' : ''}">${elf.total_entropy.toFixed(3)}</span></div>
</div>`;
// ELF Header card
const hdr = elf.header;
html += `<div class="elf-card">
<div class="elf-card-title">ELF HEADER</div>
<div class="elf-field"><span class="elf-label">Type</span><span class="elf-value">${escapeHtml(hdr.type)}</span></div>
<div class="elf-field"><span class="elf-label">Machine</span><span class="elf-value">${escapeHtml(hdr.machine)}</span></div>
<div class="elf-field"><span class="elf-label">Entry Point</span><span class="elf-value mono">${hdr.entry_point}</span></div>
<div class="elf-field"><span class="elf-label">Sections</span><span class="elf-value">${hdr.sh_count}</span></div>
<div class="elf-field"><span class="elf-label">Segments</span><span class="elf-value">${hdr.ph_count}</span></div>
<div class="elf-field"><span class="elf-label">Flags</span><span class="elf-value mono">${hdr.flags}</span></div>
</div>`;
// Security Features card
const sec = elf.security;
html += `<div class="elf-card">
<div class="elf-card-title">SECURITY FEATURES</div>
<div class="elf-field"><span class="elf-label">PIE (ASLR)</span><span class="elf-value ${sec.pie ? 'elf-ok' : 'elf-bad'}">${sec.pie ? 'Yes' : 'No'}</span></div>
<div class="elf-field"><span class="elf-label">NX (Stack)</span><span class="elf-value ${sec.nx ? 'elf-ok' : 'elf-bad'}">${sec.nx ? 'Enabled' : 'Disabled'}</span></div>
<div class="elf-field"><span class="elf-label">RELRO</span><span class="elf-value ${sec.relro === 'Full' ? 'elf-ok' : sec.relro === 'Partial' ? 'elf-warn' : 'elf-bad'}">${sec.relro}</span></div>
<div class="elf-field"><span class="elf-label">Stack Canary</span><span class="elf-value ${sec.stack_canary ? 'elf-ok' : 'elf-bad'}">${sec.stack_canary ? 'Yes' : 'No'}</span></div>
<div class="elf-field"><span class="elf-label">Fortify</span><span class="elf-value ${sec.fortify ? 'elf-ok' : 'elf-dim'}">${sec.fortify ? 'Yes' : 'No'}</span></div>
<div class="elf-field"><span class="elf-label">Stripped</span><span class="elf-value">${sec.stripped ? 'Yes' : 'No'}</span></div>
</div>`;
// Linking info card
html += `<div class="elf-card">
<div class="elf-card-title">LINKING</div>
<div class="elf-field"><span class="elf-label">Interpreter</span><span class="elf-value mono">${escapeHtml(elf.interpreter || 'None (static)')}</span></div>
<div class="elf-field"><span class="elf-label">SONAME</span><span class="elf-value">${escapeHtml(elf.soname || '—')}</span></div>
<div class="elf-field"><span class="elf-label">RPATH</span><span class="elf-value ${elf.rpath ? 'elf-warn' : ''}">${escapeHtml(elf.rpath || '—')}</span></div>
<div class="elf-field"><span class="elf-label">RUNPATH</span><span class="elf-value">${escapeHtml(elf.runpath || '—')}</span></div>
<div class="elf-field"><span class="elf-label">Libraries</span><span class="elf-value">${elf.needed_libraries ? elf.needed_libraries.length : 0}</span></div>
<div class="elf-field"><span class="elf-label">Imports</span><span class="elf-value">${elf.import_count || 0} symbols</span></div>
</div>`;
html += '</div>'; // end overview grid
// --- Needed Libraries ---
if (elf.needed_libraries && elf.needed_libraries.length > 0) {
html += '<div class="elf-section-block">';
html += '<div class="elf-card-title">NEEDED LIBRARIES</div>';
html += '<div class="elf-libs-list">';
elf.needed_libraries.forEach(lib => {
html += `<span class="elf-lib-badge">${escapeHtml(lib)}</span>`;
});
html += '</div></div>';
}
// --- Sections Table ---
if (elf.sections && elf.sections.length > 0) {
html += '<div class="elf-section-block">';
html += '<div class="elf-card-title">SECTIONS</div>';
html += '<table class="elf-table"><thead><tr><th>#</th><th>Name</th><th>Type</th><th>Address</th><th>Offset</th><th>Size</th><th>Flags</th><th>Entropy</th><th>Status</th></tr></thead><tbody>';
elf.sections.forEach(s => {
const rowClass = s.entropy_status === 'high' ? 'elf-row-high' : s.entropy_status === 'warn' ? 'elf-row-warn' : '';
const wxBadge = s.wx_warning ? ' <span class="elf-wx-badge">W+X</span>' : '';
const packerBadge = s.packer_indicator ? ` <span class="elf-packer-badge">${escapeHtml(s.packer_indicator)}</span>` : '';
const elfSecOffsetDec = s.offset_dec != null ? s.offset_dec : parseInt(s.offset, 16);
const nameHtml = s.name && s.size > 0
? `<a class="section-link" href="#" onclick="elfJumpToSection(${elfSecOffsetDec}); return false;" title="View in Hex Editor">${escapeHtml(s.name)}</a>`
: (escapeHtml(s.name) || '<em>null</em>');
html += `<tr class="${rowClass}">
<td>${s.index}</td>
<td class="mono">${nameHtml}${wxBadge}${packerBadge}</td>
<td>${escapeHtml(s.type)}</td>
<td class="mono">${s.address}</td>
<td class="mono">${s.offset}</td>
<td>${formatSize(s.size)}</td>
<td class="mono">${s.flags_str}</td>
<td><div class="elf-entropy-bar"><div class="elf-entropy-fill ${s.entropy_status}" style="width:${(s.entropy / 8 * 100).toFixed(1)}%"></div><span class="elf-entropy-val">${s.entropy.toFixed(2)}</span></div></td>
<td><span class="elf-status-badge ${s.entropy_status}">${s.entropy_status}</span></td>
</tr>`;
});
html += '</tbody></table></div>';
}
// --- Segments Table ---
if (elf.segments && elf.segments.length > 0) {
html += '<div class="elf-section-block">';
html += '<div class="elf-card-title">PROGRAM HEADERS (SEGMENTS)</div>';
html += '<table class="elf-table"><thead><tr><th>#</th><th>Type</th><th>Offset</th><th>VAddr</th><th>FileSz</th><th>MemSz</th><th>Flags</th><th>Note</th></tr></thead><tbody>';
elf.segments.forEach(seg => {
const noteHtml = seg.security_note ? `<span class="elf-seg-note">${escapeHtml(seg.security_note)}</span>` :
seg.interpreter ? `<span class="elf-seg-note mono">${escapeHtml(seg.interpreter)}</span>` : '';
html += `<tr>
<td>${seg.index}</td>
<td class="mono">${escapeHtml(seg.type)}</td>
<td class="mono">${seg.offset}</td>
<td class="mono">${seg.vaddr}</td>
<td>${formatSize(seg.filesz)}</td>
<td>${formatSize(seg.memsz)}</td>
<td class="mono">${seg.flags_str}</td>
<td>${noteHtml}</td>
</tr>`;
});
html += '</tbody></table></div>';
}
// --- Suspicious Imports ---
if (elf.suspicious_imports && Object.keys(elf.suspicious_imports).length > 0) {
html += '<div class="elf-section-block">';
html += '<div class="elf-card-title elf-suspicious-title">SUSPICIOUS IMPORTS</div>';
html += '<div class="elf-suspicious-grid">';
for (const [category, symbols] of Object.entries(elf.suspicious_imports)) {
const catLabel = category.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
html += `<div class="elf-suspicious-card">
<div class="elf-suspicious-cat">${escapeHtml(catLabel)}</div>
<div class="elf-suspicious-syms">${symbols.map(s => `<code>${escapeHtml(s)}</code>`).join(', ')}</div>
</div>`;
}
html += '</div></div>';
}
// --- Imported Symbols (collapsible) ---
if (elf.imported_symbols && elf.imported_symbols.length > 0) {
html += '<div class="elf-section-block">';
html += `<div class="elf-card-title elf-collapsible" onclick="elfToggleSection(this)">IMPORTED SYMBOLS (${elf.import_count}) &#x25B6;</div>`;
html += '<div class="elf-collapse-body" style="display:none;">';
html += '<table class="elf-table elf-sym-table"><thead><tr><th>Name</th><th>Bind</th><th>Type</th></tr></thead><tbody>';
elf.imported_symbols.forEach(sym => {
html += `<tr><td class="mono">${escapeHtml(sym.name)}</td><td>${sym.bind}</td><td>${sym.type}</td></tr>`;
});
if (elf.import_count > elf.imported_symbols.length) {
html += `<tr><td colspan="3" class="muted">... and ${elf.import_count - elf.imported_symbols.length} more</td></tr>`;
}
html += '</tbody></table></div></div>';
}
// --- Exported Symbols (collapsible) ---
if (elf.exported_symbols && elf.exported_symbols.length > 0) {
html += '<div class="elf-section-block">';
html += `<div class="elf-card-title elf-collapsible" onclick="elfToggleSection(this)">EXPORTED SYMBOLS (${elf.export_count}) &#x25B6;</div>`;
html += '<div class="elf-collapse-body" style="display:none;">';
html += '<table class="elf-table elf-sym-table"><thead><tr><th>Name</th><th>Bind</th><th>Type</th><th>Value</th><th>Size</th></tr></thead><tbody>';
elf.exported_symbols.forEach(sym => {
html += `<tr><td class="mono">${escapeHtml(sym.name)}</td><td>${sym.bind}</td><td>${sym.type}</td><td class="mono">${sym.value}</td><td>${sym.size}</td></tr>`;
});
if (elf.export_count > elf.exported_symbols.length) {
html += `<tr><td colspan="5" class="muted">... and ${elf.export_count - elf.exported_symbols.length} more</td></tr>`;
}
html += '</tbody></table></div></div>';
}
container.innerHTML = html;
}
function elfToggleSection(el) {
const body = el.nextElementSibling;
if (body.style.display === 'none') {
body.style.display = 'block';
el.innerHTML = el.innerHTML.replace('\u25B6', '\u25BC');
} else {
body.style.display = 'none';
el.innerHTML = el.innerHTML.replace('\u25BC', '\u25B6');
}
}
function elfJumpToSection(rawOffset) {
// Load section in hex editor at this offset
document.getElementById('hex-offset').value = rawOffset;
state.hexOffset = rawOffset;
hexLoad();
// Scroll to hex editor body
const editorBody = document.querySelector('.hex-editor-body');
if (editorBody) editorBody.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
// --- Detail Panels for Dashboard Services ---
async function openAgentDetail() {
pushDetailHistory('agent', 0);
@@ -1985,11 +2233,271 @@ async function openLitterboxDetail() {
let html = `<div class="detail-fields">
<div class="detail-field"><span class="field-label">Status</span><span class="field-value" style="color:${online ? 'var(--accent-green)' : 'var(--accent-red)'}">${online ? 'Running' : 'Stopped'}</span></div>
<div class="detail-field"><span class="field-label">Port</span><span class="field-value">1337</span></div>
<div class="detail-field"><span class="field-label">Install Dir</span><span class="field-value mono">C:\\LitterBox</span></div>
<div class="detail-field"><span class="field-label">URL</span><span class="field-value"><a href="http://localhost:1337" target="_blank" style="color:var(--accent-cyan)">http://localhost:1337</a></span></div>
</div>`;
// Fetch recent analyses from LitterBox
if (online) {
try {
const resp = await fetch('/api/litterbox/analyses?status=completed');
if (resp.ok) {
const analyses = await resp.json();
const items = Array.isArray(analyses) ? analyses : (analyses.analyses || analyses.results || []);
if (items.length > 0) {
html += `<div class="detail-section"><div class="detail-section-title">RECENT ANALYSES (${items.length})</div>`;
html += '<div class="lb-analyses-list">';
items.slice(0, 20).forEach(a => {
const hash = a.sha256 || a.hash || a.id || '';
const filename = a.filename || a.name || hash.substring(0, 12) + '...';
const score = a.score !== undefined ? a.score : '--';
const status = a.status || 'completed';
const scoreClass = score >= 7 ? 'high' : score >= 4 ? 'med' : 'low';
const ts = a.timestamp || a.created_at || '';
const timeStr = ts ? new Date(ts).toLocaleString('en-GB', {hour12:false, day:'2-digit', month:'short', hour:'2-digit', minute:'2-digit'}) : '--';
html += `<div class="lb-analysis-entry" onclick="viewLitterboxResult('${escapeHtml(hash)}')">
<div class="lb-entry-main">
<span class="lb-entry-name" title="${escapeHtml(hash)}">${escapeHtml(filename)}</span>
<span class="lb-entry-time">${timeStr}</span>
</div>
<div class="lb-entry-meta">
<span class="lb-entry-score ${scoreClass}">${score}</span>
<span class="lb-entry-hash mono">${hash.substring(0, 16)}...</span>
<span class="lb-entry-status">${status}</span>
</div>
</div>`;
});
html += '</div></div>';
} else {
html += '<div class="detail-section"><div class="detail-section-title">RECENT ANALYSES</div><div class="muted" style="padding:8px;font-size:11px;">No completed analyses found.</div></div>';
}
}
} catch (e) {
html += `<div class="detail-section"><div class="muted" style="padding:8px;font-size:11px;">Could not fetch analyses: ${escapeHtml(e.message)}</div></div>`;
}
} else {
html += '<div class="detail-section"><div class="muted" style="padding:12px;font-size:11px;">LitterBox is offline. Start the service to view analyses.</div></div>';
}
setDetailBody(html);
}
async function viewLitterboxResult(hash) {
pushDetailHistory('litterbox-result', hash);
setDetailHeader('Analysis', 'background:rgba(167,139,250,0.15);color:var(--accent-purple)', 'LitterBox Result', hash.substring(0, 12) + '...');
setDetailBody('<div class="muted">Fetching analysis results...</div>');
showDetail();
let html = '';
// Fetch static results
let staticData = null;
try {
const resp = await fetch(`/api/litterbox/results/static/${encodeURIComponent(hash)}`);
if (resp.ok) staticData = await resp.json();
} catch (e) {}
// Fetch dynamic results
let dynamicData = null;
try {
const resp = await fetch(`/api/litterbox/results/dynamic/${encodeURIComponent(hash)}`);
if (resp.ok) dynamicData = await resp.json();
} catch (e) {}
// Fetch file info
let fileInfo = null;
try {
const resp = await fetch(`/api/litterbox/results/info/${encodeURIComponent(hash)}`);
if (resp.ok) fileInfo = await resp.json();
} catch (e) {}
// File info section
if (fileInfo) {
html += `<div class="detail-fields">`;
if (fileInfo.filename || fileInfo.name) html += `<div class="detail-field"><span class="field-label">Filename</span><span class="field-value">${escapeHtml(fileInfo.filename || fileInfo.name)}</span></div>`;
if (fileInfo.sha256) html += `<div class="detail-field"><span class="field-label">SHA-256</span><span class="field-value mono" style="font-size:10px;word-break:break-all">${escapeHtml(fileInfo.sha256)}</span></div>`;
if (fileInfo.md5) html += `<div class="detail-field"><span class="field-label">MD5</span><span class="field-value mono" style="font-size:10px">${escapeHtml(fileInfo.md5)}</span></div>`;
if (fileInfo.size !== undefined) html += `<div class="detail-field"><span class="field-label">Size</span><span class="field-value">${formatSize(fileInfo.size)}</span></div>`;
if (fileInfo.file_type || fileInfo.type) html += `<div class="detail-field"><span class="field-label">Type</span><span class="field-value">${escapeHtml(fileInfo.file_type || fileInfo.type)}</span></div>`;
if (fileInfo.score !== undefined) html += `<div class="detail-field"><span class="field-label">Score</span><span class="field-value" style="color:${fileInfo.score >= 7 ? 'var(--accent-red)' : fileInfo.score >= 4 ? '#fbbf24' : 'var(--accent-green)'};font-weight:700">${fileInfo.score}/10</span></div>`;
html += `</div>`;
} else {
html += `<div class="detail-fields"><div class="detail-field"><span class="field-label">Hash</span><span class="field-value mono" style="font-size:10px;word-break:break-all">${escapeHtml(hash)}</span></div></div>`;
}
// Static analysis results
html += '<div class="detail-section"><div class="detail-section-title">STATIC ANALYSIS</div>';
if (staticData && !staticData.error) {
html += '<div class="lb-result-content">';
html += renderLbStaticResults(staticData);
html += '</div>';
} else {
html += `<div class="muted" style="padding:8px;font-size:11px;">${staticData?.error ? escapeHtml(staticData.error) : 'No static analysis results available.'}</div>`;
}
html += '</div>';
// Dynamic analysis results
html += '<div class="detail-section"><div class="detail-section-title">DYNAMIC ANALYSIS</div>';
if (dynamicData && !dynamicData.error) {
html += '<div class="lb-result-content">';
html += renderLbDynamicResults(dynamicData);
html += '</div>';
} else {
html += `<div class="muted" style="padding:8px;font-size:11px;">${dynamicData?.error ? escapeHtml(dynamicData.error) : 'No dynamic analysis results available.'}</div>`;
}
html += '</div>';
// Link to LitterBox UI
html += `<div style="margin-top:12px;"><a href="http://localhost:1337" target="_blank" class="btn btn-sm" style="color:var(--accent-purple);border-color:rgba(167,139,250,0.3);">Open in LitterBox UI</a></div>`;
setDetailBody(html);
}
function renderLbStaticResults(data) {
let html = '';
// YARA matches
const yara = data.yara_results || data.yara || data.yara_matches;
if (yara) {
const matches = Array.isArray(yara) ? yara : (yara.matches || yara.rules || []);
if (matches.length > 0) {
html += `<div class="lb-subsection"><div class="lb-sub-title">YARA Matches (${matches.length})</div><div class="lb-tag-list">`;
matches.forEach(m => {
const name = typeof m === 'string' ? m : (m.rule || m.name || JSON.stringify(m));
html += `<span class="lb-yara-tag">${escapeHtml(name)}</span>`;
});
html += '</div></div>';
}
}
// CheckPlz results
const checkplz = data.checkplz_results || data.checkplz;
if (checkplz) {
html += '<div class="lb-subsection"><div class="lb-sub-title">CheckPlz</div>';
if (typeof checkplz === 'object') {
const detections = checkplz.detections || checkplz.results || [];
if (Array.isArray(detections) && detections.length > 0) {
html += '<div class="lb-tag-list">';
detections.forEach(d => {
const label = typeof d === 'string' ? d : (d.name || d.rule || JSON.stringify(d));
html += `<span class="lb-detection-tag">${escapeHtml(label)}</span>`;
});
html += '</div>';
} else {
html += `<pre class="lb-raw">${escapeHtml(JSON.stringify(checkplz, null, 2)).substring(0, 1500)}</pre>`;
}
} else {
html += `<pre class="lb-raw">${escapeHtml(String(checkplz)).substring(0, 1500)}</pre>`;
}
html += '</div>';
}
// Strings / Stringnalyzer
const strings = data.stringnalyzer_results || data.strings || data.stringnalyzer;
if (strings) {
html += '<div class="lb-subsection"><div class="lb-sub-title">Strings Analysis</div>';
if (typeof strings === 'object') {
const suspicious = strings.suspicious || strings.suspicious_strings || [];
const count = strings.count || strings.total || (Array.isArray(suspicious) ? suspicious.length : 0);
html += `<div class="lb-meta">Suspicious strings: <strong>${count}</strong></div>`;
if (Array.isArray(suspicious) && suspicious.length > 0) {
html += '<div class="lb-strings-list">';
suspicious.slice(0, 30).forEach(s => {
const val = typeof s === 'string' ? s : (s.value || s.string || JSON.stringify(s));
html += `<div class="lb-string-entry mono">${escapeHtml(val)}</div>`;
});
if (suspicious.length > 30) html += `<div class="lb-string-entry muted">... and ${suspicious.length - 30} more</div>`;
html += '</div>';
}
} else {
html += `<pre class="lb-raw">${escapeHtml(String(strings)).substring(0, 1500)}</pre>`;
}
html += '</div>';
}
// Fallback: raw data if nothing matched above
if (!yara && !checkplz && !strings) {
html += `<pre class="lb-raw">${escapeHtml(JSON.stringify(data, null, 2)).substring(0, 3000)}</pre>`;
}
return html;
}
function renderLbDynamicResults(data) {
let html = '';
// PE-Sieve
const peSieve = data.pe_sieve || data.pe_sieve_results;
if (peSieve) {
html += '<div class="lb-subsection"><div class="lb-sub-title">PE-Sieve</div>';
if (typeof peSieve === 'object') {
const suspicious = peSieve.suspicious || peSieve.total_suspicious || 0;
const replaced = peSieve.replaced || peSieve.total_replaced || 0;
const implanted = peSieve.implanted || 0;
html += `<div class="lb-dynamic-stats">`;
html += `<span class="lb-stat ${suspicious > 0 ? 'warn' : 'ok'}">Suspicious: ${suspicious}</span>`;
html += `<span class="lb-stat ${replaced > 0 ? 'bad' : 'ok'}">Replaced: ${replaced}</span>`;
html += `<span class="lb-stat ${implanted > 0 ? 'bad' : 'ok'}">Implanted: ${implanted}</span>`;
html += `</div>`;
if (peSieve.details || peSieve.modules) {
html += `<details class="lb-raw-details"><summary>Raw output</summary><pre class="lb-raw">${escapeHtml(JSON.stringify(peSieve.details || peSieve.modules, null, 2)).substring(0, 2000)}</pre></details>`;
}
} else {
html += `<pre class="lb-raw">${escapeHtml(String(peSieve)).substring(0, 1500)}</pre>`;
}
html += '</div>';
}
// Moneta
const moneta = data.moneta || data.moneta_results;
if (moneta) {
html += '<div class="lb-subsection"><div class="lb-sub-title">Moneta</div>';
if (typeof moneta === 'object') {
const iocs = moneta.ioc_count || moneta.iocs || moneta.findings || 0;
const iocCount = typeof iocs === 'number' ? iocs : (Array.isArray(iocs) ? iocs.length : 0);
html += `<div class="lb-dynamic-stats"><span class="lb-stat ${iocCount > 0 ? 'bad' : 'ok'}">IOCs: ${iocCount}</span></div>`;
if (Array.isArray(iocs) && iocs.length > 0) {
html += '<div class="lb-tag-list">';
iocs.slice(0, 20).forEach(ioc => {
const label = typeof ioc === 'string' ? ioc : (ioc.description || ioc.type || JSON.stringify(ioc));
html += `<span class="lb-detection-tag">${escapeHtml(label)}</span>`;
});
html += '</div>';
}
} else {
html += `<pre class="lb-raw">${escapeHtml(String(moneta)).substring(0, 1500)}</pre>`;
}
html += '</div>';
}
// HollowsHunter
const hollows = data.hollows_hunter || data.hollows_hunter_results;
if (hollows) {
html += '<div class="lb-subsection"><div class="lb-sub-title">HollowsHunter</div>';
if (typeof hollows === 'object') {
const suspicious = hollows.suspicious || hollows.total_suspicious || 0;
html += `<div class="lb-dynamic-stats"><span class="lb-stat ${suspicious > 0 ? 'bad' : 'ok'}">Suspicious: ${suspicious}</span></div>`;
} else {
html += `<pre class="lb-raw">${escapeHtml(String(hollows)).substring(0, 1500)}</pre>`;
}
html += '</div>';
}
// RedEdr
const rededr = data.rededr || data.rededr_results;
if (rededr) {
html += '<div class="lb-subsection"><div class="lb-sub-title">RedEdr</div>';
html += `<pre class="lb-raw">${escapeHtml(typeof rededr === 'object' ? JSON.stringify(rededr, null, 2) : String(rededr)).substring(0, 2000)}</pre>`;
html += '</div>';
}
// Fallback
if (!peSieve && !moneta && !hollows && !rededr) {
html += `<pre class="lb-raw">${escapeHtml(JSON.stringify(data, null, 2)).substring(0, 3000)}</pre>`;
}
return html;
}
async function openRustinelDetail() {
pushDetailHistory('rustinel', 0);
setDetailHeader('Engine', 'background:rgba(34,211,238,0.15);color:var(--accent-cyan)', 'Rustinel', 'loading...');
@@ -2313,7 +2821,7 @@ function updateServiceStatus(status) {
setStatus('status-sysmon', status.sysmon?.online);
setStatus('status-agent', status.detonator_agent?.online);
setStatus('status-litterbox', status.litterbox?.online);
setStatus('status-fibratus', status.rustinel?.online);
setStatus('status-fibratus', status.fibratus?.online);
}
function setStatus(elementId, online) {
@@ -2324,6 +2832,51 @@ function setStatus(elementId, online) {
}
}
async function launchService(serviceName) {
const btn = event.currentTarget;
const originalText = btn.textContent;
btn.textContent = 'Starting...';
btn.disabled = true;
btn.classList.add('launching');
try {
const resp = await fetch('/api/service/launch', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({service: serviceName}),
});
const data = await resp.json();
if (data.success) {
btn.textContent = 'Launched';
btn.classList.remove('launching');
btn.classList.add('launched');
// 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);
setTimeout(() => {
btn.textContent = originalText;
btn.disabled = false;
btn.classList.remove('launch-failed');
}, 3000);
}
} catch (e) {
btn.textContent = 'Error';
btn.classList.remove('launching');
btn.classList.add('launch-failed');
console.error('Launch error:', e);
setTimeout(() => {
btn.textContent = originalText;
btn.disabled = false;
btn.classList.remove('launch-failed');
}, 3000);
}
}
// --- Detail Panel: Navigation ---
function pushDetailHistory(type, id) {
const last = state.detailHistory[state.detailHistory.length - 1];
@@ -2533,13 +3086,13 @@ function renderDetonationResults(data, container) {
// Start polling for results
if (sha256 || pid || lbHash) {
pollDetonationResults(sha256, pid, lbHash, 0);
pollDetonationResults(sha256, pid, lbHash, filename, 0);
}
}
let _detonationPollTimer = null;
function pollDetonationResults(sha256, pid, lbHash, attempt) {
function pollDetonationResults(sha256, pid, lbHash, filename, attempt) {
if (_detonationPollTimer) clearTimeout(_detonationPollTimer);
const maxAttempts = 30; // Poll for up to ~2.5 minutes
if (attempt >= maxAttempts) {
@@ -2552,6 +3105,7 @@ function pollDetonationResults(sha256, pid, lbHash, attempt) {
if (sha256) params.set('sha256', sha256);
if (pid) params.set('pid', pid);
if (lbHash) params.set('litterbox_hash', lbHash);
if (filename) params.set('filename', filename);
fetch(`/api/detonation/results?${params}`)
.then(r => r.json())
@@ -2564,14 +3118,24 @@ function pollDetonationResults(sha256, pid, lbHash, attempt) {
fStage.querySelector('.det-stage-icon').innerHTML = '&#x2705;';
fStage.querySelector('.det-stage-detail').textContent = `${data.fibratus_alert_count} alert(s) detected`;
}
// Keep polling if not all results are ready
const allReady = data.ready && data.ready.static !== false && data.ready.dynamic !== false;
if (!allReady || attempt < 5) {
_detonationPollTimer = setTimeout(() => pollDetonationResults(sha256, pid, lbHash, attempt + 1), 5000);
// 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
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) {
_detonationPollTimer = setTimeout(() => pollDetonationResults(sha256, pid, lbHash, filename, attempt + 1), 5000);
} else {
// Final update: show polling complete message
const panels = document.getElementById('det-results-panels');
if (panels && !panels.querySelector('.det-poll-done')) {
panels.insertAdjacentHTML('beforeend', '<div class="det-poll-done">Polling complete. All results collected.</div>');
}
}
})
.catch(() => {
_detonationPollTimer = setTimeout(() => pollDetonationResults(sha256, pid, lbHash, attempt + 1), 5000);
_detonationPollTimer = setTimeout(() => pollDetonationResults(sha256, pid, lbHash, filename, attempt + 1), 5000);
});
}
@@ -2587,13 +3151,16 @@ function renderDetonationPanels(data) {
<div class="det-panel-title">FIBRATUS / RUSTINEL ALERTS (${data.fibratus_alert_count})</div>
<div class="det-alerts-list">`;
data.fibratus_alerts.slice(0, 20).forEach(alert => {
const sev = (alert.severity || alert.rule?.level || 'unknown').toLowerCase();
const ruleName = alert.rule_name || alert.rule?.name || 'Unknown Rule';
const procName = alert.process?.name || '';
const sev = (alert.severity || 'unknown').toLowerCase();
const ruleName = alert.rule_name || 'Unknown Rule';
const procName = alert.process_name || '';
const engine = alert.engine || '';
const pid = alert.pid || '';
html += `<div class="det-alert-item sev-${sev}">
<span class="det-alert-sev">${sev.toUpperCase()}</span>
<span class="det-alert-rule">${escapeHtml(ruleName)}</span>
<span class="det-alert-proc">${escapeHtml(procName)}</span>
<span class="det-alert-proc">${escapeHtml(procName)}${pid ? ' (PID:' + pid + ')' : ''}</span>
${engine ? '<span class="det-alert-engine">' + escapeHtml(engine) + '</span>' : ''}
</div>`;
});
html += `</div></div>`;
@@ -2757,9 +3324,14 @@ async function refreshSubmissions() {
: '';
const pid = sub.agent_pid ? `<span class="badge badge-dim">PID ${sub.agent_pid}</span>` : '';
const shortHash = sub.sha256 ? sub.sha256.substring(0, 12) + '...' : '--';
const actions = sub.file_path
let actions = sub.file_path
? `<button class="btn btn-xs" onclick="hexOpenFile('${escapeHtml(sub.file_path.replace(/\\/g, '\\\\'))}')" title="Open in Hex Editor">Hex</button>`
: '';
// Add LitterBox results button if submission went to LitterBox
const lbHash = sub.litterbox_hash || sub.sha256;
if (lbHash && (sub.target === 'litterbox' || sub.target === 'both' || sub.litterbox_status === 'success')) {
actions += ` <button class="btn btn-xs btn-lb-results" onclick="viewLitterboxResult('${escapeHtml(lbHash)}')" title="View LitterBox scan results">Results</button>`;
}
html += `<tr>`;
html += `<td class="td-time">${ts}</td>`;
@@ -2902,7 +3474,10 @@ function buildGraph(processes, networkEvents, dnsEvents, injectEvents, networkAl
const includePids = new Set([...alertPids, ...sysmonPids]);
for (const pid of [...alertPids]) {
const proc = processes[pid];
if (proc?.parent_pid && processes[proc.parent_pid]) includePids.add(String(proc.parent_pid));
if (proc?.parent_pid != null) {
const ppidStr = String(proc.parent_pid);
if (processes[ppidStr]) includePids.add(ppidStr);
}
(proc?.children || []).forEach(c => includePids.add(String(c)));
}
@@ -2924,7 +3499,10 @@ function buildGraph(processes, networkEvents, dnsEvents, injectEvents, networkAl
const expandedPids = new Set(matchedPids);
for (const pid of matchedPids) {
const proc = processes[pid];
if (proc?.parent_pid && processes[proc.parent_pid]) expandedPids.add(String(proc.parent_pid));
if (proc?.parent_pid != null) {
const ppidStr = String(proc.parent_pid);
if (processes[ppidStr]) expandedPids.add(ppidStr);
}
(proc?.children || []).forEach(c => { if (includePids.has(String(c))) expandedPids.add(String(c)); });
}
// Replace includePids with search-filtered set
@@ -2943,7 +3521,10 @@ function buildGraph(processes, networkEvents, dnsEvents, injectEvents, networkAl
const expandedDet = new Set(detonatedPids);
for (const pid of detonatedPids) {
const proc = processes[pid];
if (proc?.parent_pid && processes[proc.parent_pid]) expandedDet.add(String(proc.parent_pid));
if (proc?.parent_pid != null) {
const ppidStr = String(proc.parent_pid);
if (processes[ppidStr]) expandedDet.add(ppidStr);
}
(proc?.children || []).forEach(c => { if (includePids.has(String(c))) expandedDet.add(String(c)); });
}
includePids.clear();
@@ -2980,9 +3561,10 @@ function buildGraph(processes, networkEvents, dnsEvents, injectEvents, networkAl
// 2. Create parent-child edges
for (const node of nodes) {
if (node.parentPid && nodeMap[node.parentPid]) {
const ppid = node.parentPid != null ? String(node.parentPid) : null;
if (ppid && nodeMap[ppid]) {
edges.push({
source: `proc_${node.parentPid}`,
source: `proc_${ppid}`,
target: node.id,
type: 'spawn',
label: 'spawned',
@@ -3541,6 +4123,23 @@ function renderGraph() {
const { nodes, edges, camera } = graphState;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Empty state message
if (nodes.length === 0) {
ctx.save();
ctx.fillStyle = '#64748b';
ctx.font = '14px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const showDetonatedOnly = document.getElementById('graph-filter-detonated')?.checked;
const msg = showDetonatedOnly
? 'No detonated processes found. Submit a sample to see detonation activity.'
: 'No process data available.';
ctx.fillText(msg, canvas.width / 2, canvas.height / 2);
ctx.restore();
return;
}
ctx.save();
ctx.translate(camera.x, camera.y);
ctx.scale(camera.zoom, camera.zoom);
@@ -3620,6 +4219,11 @@ function renderGraph() {
else if (node.severity === 'medium') color = '#eab308';
}
// Override color for detonated processes (gold)
if (node.detonated) {
color = '#fbbf24';
}
const r = node.radius * (isHovered ? 1.2 : 1);
// Glow for malicious
@@ -3630,6 +4234,14 @@ function renderGraph() {
ctx.fill();
}
// Glow for detonated
if (node.detonated) {
ctx.beginPath();
ctx.arc(node.x, node.y, r + 5, 0, Math.PI * 2);
ctx.fillStyle = '#fbbf2425';
ctx.fill();
}
// Node circle
ctx.beginPath();
ctx.arc(node.x, node.y, r, 0, Math.PI * 2);
+48
View File
@@ -1,4 +1,52 @@
[
{
"id": "66187199fb12",
"timestamp": "2026-06-10T11:21:06.849116",
"filename": "npp.8.9.6.2.Installer.x64.exe",
"sha256": "7c243203265ce8fdac76c839bf744ae35dcf620760eb97c2ea279af498560e45",
"size": 6898288,
"target": "both",
"agent_status": "success",
"agent_pid": 20928,
"litterbox_status": "success",
"file_path": "C:\\Users\\vagrant\\Desktop\\infected\\npp.8.9.6.2.Installer.x64.exe"
},
{
"id": "8d17177fbadf",
"timestamp": "2026-06-10T11:18:16.974881",
"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": "fb3cf386bb11",
"timestamp": "2026-06-10T10:03:22.530395",
"filename": "mimikatz.exe",
"sha256": "61c0810a23580cf492a6ba4f7654566108331e7a4134c968c2d6a05261b2d8a1",
"size": 1355264,
"target": "litterbox",
"agent_status": null,
"agent_pid": null,
"litterbox_status": "success",
"file_path": "C:\\Users\\vagrant\\Desktop\\infected\\mimikatz.exe"
},
{
"id": "055d7221bf3c",
"timestamp": "2026-06-10T08:34:02.112603",
"filename": "Bytecode Viewer.jar",
"sha256": "dc5f6669409d7d0bbba40c735875a39960c1777f11bb13a1819bb12917808c5c",
"size": 58554098,
"target": "both",
"agent_status": "failed",
"agent_pid": null,
"litterbox_status": "failed",
"file_path": "C:\\Users\\vagrant\\Desktop\\infected\\Bytecode Viewer.jar"
},
{
"id": "88f52877f8ed",
"timestamp": "2026-06-09T16:44:30.886477",
+13 -2
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Detonation Chamber</title>
<link rel="stylesheet" href="/static/css/style.css?v=10">
<link rel="stylesheet" href="/static/css/style.css?v=11">
</head>
<body>
<!-- Global loading overlay (shown on slow fetches) -->
@@ -366,6 +366,7 @@
<h2>Hex Editor</h2>
<div class="header-actions">
<button class="btn btn-sm btn-accent" id="pe-analyze-btn" onclick="peAnalyze()" title="Parse PE header">PE Analysis</button>
<button class="btn btn-sm btn-accent" id="elf-analyze-btn" onclick="elfAnalyze()" title="Parse ELF header">ELF Analysis</button>
<input type="number" id="hex-offset" class="filter-input hex-offset-input" placeholder="Offset" value="0" min="0">
<select id="hex-bytes-per-page" class="filter-select">
<option value="256">256 bytes</option>
@@ -439,6 +440,16 @@
<div class="pe-empty">Load a PE file and click "PE Analysis" to inspect headers.</div>
</div>
</div>
<!-- ELF Analysis Panel -->
<div class="elf-panel" id="elf-panel" style="display:none;">
<div class="elf-panel-header">
<h3>ELF Binary Analysis</h3>
<button class="btn btn-sm btn-close" onclick="document.getElementById('elf-panel').style.display='none'">&#x2715;</button>
</div>
<div class="elf-panel-body" id="elf-panel-body">
<div class="elf-empty">Load an ELF binary and click "ELF Analysis" to inspect headers, segments, symbols, and security features.</div>
</div>
</div>
</div>
<!-- Submit Tab -->
@@ -755,6 +766,6 @@ C:\Users\vagrant\Desktop\infected\ Malware samples (Defender-excluded)</pre>
</div>
</div>
<script src="/static/js/app.js?v=8"></script>
<script src="/static/js/app.js?v=9"></script>
</body>
</html>