mirror of
https://github.com/BenjiTrapp/transportable-detonation-chamber
synced 2026-08-09 12:01:14 +00:00
Provisioning
This commit is contained in:
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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">⚠</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">✅</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}) ▶</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}) ▶</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 = '✅';
|
||||
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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'">✕</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>
|
||||
|
||||
Reference in New Issue
Block a user