'use strict'; const vscode = require('vscode'); const cp = require('child_process'); const fs = require('fs'); const http = require('http'); const path = require('path'); const TOOLS = [ ['Editor', 'hex', 'Hex Editor', 'Browse and patch bytes in the working copy.'], ['Triage', 'overview', 'Overview', 'Fast first-pass summary across hashes, strings, entropy and IOCs.'], ['Triage', 'hashes', 'Hashes', 'CRC32, MD5, SHA-1, SHA-256 and SHA-512.'], ['Triage', 'entropy', 'Entropy', 'Windowed entropy for packed or encrypted regions.'], ['Triage', 'strings', 'Strings', 'ASCII and UTF-16 string extraction.'], ['Triage', 'iocs', 'IOCs', 'URLs, domains, hashes, paths and indicators.'], ['Find', 'search', 'Search', 'Text, hex or regular-expression search.'], ['Find', 'multi-search', 'Multi-search', 'Batch text, hex and regex searches.'], ['Structure', 'parse', 'Structures', 'PE, ELF, Mach-O, ZIP, images, RIFF, ASN.1 and certificates.'], ['Structure', 'disasm', 'Disasm', 'Heuristic disassembly for common CPU families.'], ['Structure', 'pdf', 'PDF', 'Objects, streams, xrefs and suspicious PDF surface.'], ['Structure', 'squashfs', 'SquashFS', 'Superblock and directory metadata.'], ['Audio', 'spectrogram-encode', 'Spectrogram Encode', 'Convert images into WAV spectrogram payloads.'], ['Audio', 'spectrogram-view', 'Spectrogram View', 'Render audio files as spectrogram images.'], ['Compare', 'diff', 'Diff', 'Changed byte regions against a compare file.'], ['Recover', 'carve', 'Carve', 'Embedded file carving candidates.'], ['Recover', 'keys', 'Keys', 'PEM, DER, OpenSSH and PKCS#12 artefact discovery.'], ['Recover', 'timestamps', 'Timestamps', 'UNIX, FILETIME, WebKit and OLE timestamp heuristics.'], ['Recover', 'stego', 'Stego', 'Appended payloads and embedded trailer formats.'], ['Recover', 'metadata', 'Metadata', 'PNG, JPEG and GIF textual metadata extraction.'], ['Advanced', 'binsnoop', 'Binsnoop', 'Deep signature scanning through Binsnoop.'], ['Advanced', 'rules', 'Rules', 'YARA-like rule scans over the current document.'], ['Edit', 'patch-plan', 'Patch Plan', 'Validate patch scripts without modifying bytes.'], ['Edit', 'patch-apply', 'Patch Apply', 'Apply a patch script to the working copy.'], ['Edit', 'transform', 'Transforms', 'Hex, Base64, URL, XOR, inflate and cipher pipelines.'], ['Case', 'case', 'Case Export', 'Build a case archive payload from the working copy.'] ].map(([group, id, name, desc]) => ({ group, id, name, desc })); const DEFAULTS = { search: { mode: 'text', query: 'MZ', max: 1000 }, 'multi-search': { spec: 'text MZ\nhex 7f454c46\nregex https?://', max: 1000 }, parse: { kind: 'auto' }, disasm: { arch: 'x86_64', start: 0, length: 4096, max: 1000 }, entropy: { window: 4096, step: 4096 }, strings: { min: 4, max: 5000 }, iocs: { min: 5 }, carve: { max: 256 }, keys: { max: 200 }, timestamps: { max: 1000 }, rules: { source: 'rule mz { strings: $a = "MZ" condition: $a }', max: 1000 }, 'patch-plan': { source: 'set 0x00 90 90' }, 'patch-apply': { source: 'set 0x00 90 90' }, transform: { pipeline: 'hex.encode', start: 0, length: 1024 }, 'spectrogram-view': { fftSize: 2048, height: 1024, maxWidth: 1400, aspectRatio: 1, minHz: 0, maxHz: 0, brightness: 1, contrast: 1 }, hex: { start: 0, bytes: 2048, width: 16 } }; class Bridge { constructor(context) { this.context = context; this.proc = null; this.addr = null; this.token = null; } async start() { if (this.addr) return; const exe = process.platform === 'win32' ? 'nexus-vscode-bridge.exe' : 'nexus-vscode-bridge'; const bridgePath = path.join(this.context.extensionPath, 'bin', exe); if (!fs.existsSync(bridgePath)) { throw new Error(`Bridge executable not found: ${bridgePath}. Run vscode/build.sh first.`); } // VSIX extraction does not always preserve the executable bit, so ensure it. if (process.platform !== 'win32') { try { fs.chmodSync(bridgePath, 0o755); } catch (_) { /* best effort */ } } this.proc = cp.spawn(bridgePath, ['-listen', '127.0.0.1:0'], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }); this.context.subscriptions.push({ dispose: () => this.stop() }); const firstLine = await new Promise((resolve, reject) => { let buf = ''; const timer = setTimeout(() => reject(new Error('Timed out waiting for bridge startup')), 10000); this.proc.stdout.on('data', chunk => { buf += chunk.toString('utf8'); const idx = buf.indexOf('\n'); if (idx >= 0) { clearTimeout(timer); resolve(buf.slice(0, idx)); } }); this.proc.once('error', reject); this.proc.once('exit', code => reject(new Error(`Bridge exited during startup (${code})`))); }); const meta = JSON.parse(firstLine); this.addr = meta.addr; this.token = meta.token; } stop() { if (this.proc && !this.proc.killed) this.proc.kill(); this.proc = null; } async rpc(method, args = []) { await this.start(); const payload = Buffer.from(JSON.stringify({ method, args })); return new Promise((resolve, reject) => { const req = http.request({ hostname: this.addr.split(':')[0], port: Number(this.addr.split(':')[1]), path: '/rpc', method: 'POST', headers: { Authorization: `Bearer ${this.token}`, 'Content-Type': 'application/json', 'Content-Length': payload.length } }, res => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { if (res.statusCode < 200 || res.statusCode >= 300) { reject(new Error(`Bridge HTTP ${res.statusCode}: ${data}`)); return; } const decoded = JSON.parse(data || '{}'); if (decoded.error) reject(new Error(decoded.error)); else resolve(decoded.result); }); }); req.on('error', reject); req.write(payload); req.end(); }); } } class ToolsView { constructor(context, bridge) { this.context = context; this.bridge = bridge; this.view = null; this.state = null; } resolveWebviewView(view) { this.view = view; view.webview.options = { enableScripts: true }; view.webview.html = this.html(view.webview); view.webview.onDidReceiveMessage(async msg => { try { if (msg.type === 'load') await loadFile(this.bridge, false, this); if (msg.type === 'compare') await loadFile(this.bridge, true, this); if (msg.type === 'save') await saveFile(this.bridge, this); if (msg.type === 'refresh') await this.refreshState(); if (msg.type === 'tool') await openTool(this.context, this.bridge, this, msg.tool); } catch (err) { this.postError(err); } }); this.refreshState(); } async refreshState() { try { this.state = await this.bridge.rpc('LeviathanState'); } catch (_) { this.state = null; } this.postState(); } postState() { if (this.view) this.view.webview.postMessage({ type: 'state', state: this.state }); } postError(err) { if (this.view) this.view.webview.postMessage({ type: 'error', error: err.message || String(err) }); else vscode.window.showErrorMessage(err.message || String(err)); } html(webview) { const nonce = String(Date.now()); const toolsJson = JSON.stringify(TOOLS); return `
No file loadedLoad a binary or right-click a file in Explorer.
`; } } class ToolPanel { constructor(context, bridge, tool, options = {}) { this.context = context; this.bridge = bridge; this.tool = tool; this.fileState = options.fileState || null; const meta = TOOLS.find(t => t.id === tool) || { name: tool }; const title = this.fileState?.name ? `${meta.name}: ${this.fileState.name}` : meta.name; this.panel = vscode.window.createWebviewPanel('leviathanTool', title, vscode.ViewColumn.Active, { enableScripts: true, retainContextWhenHidden: true }); this.panel.webview.html = this.tool === 'hex' ? this.hexHtml(meta) : this.html(meta); this.panel.webview.onDidReceiveMessage(msg => { if (msg.type === 'run') this.run(msg.params); if (msg.type === 'hexLoad') this.loadHex(msg); if (msg.type === 'hexSet') this.setHexByte(msg); if (msg.type === 'hexSearch') this.searchHex(msg); if (msg.type === 'undo') this.stateAction('LeviathanUndo', msg); if (msg.type === 'redo') this.stateAction('LeviathanRedo', msg); if (msg.type === 'revert') this.stateAction('LeviathanRevert', msg); }); this.panel.webview.postMessage({ type: 'fileState', state: this.fileState }); this.refreshState(); if (options.autoRun) { if (this.tool === 'hex') { setTimeout(() => this.loadHex(DEFAULTS.hex), 150); } else { setTimeout(() => this.run(JSON.stringify(DEFAULTS[this.tool] || {})), 150); } } } async refreshState() { try { const state = await this.bridge.rpc('LeviathanState'); this.fileState = state; this.panel.webview.postMessage({ type: 'fileState', state }); } catch (err) { this.error(err); } } async run(paramsText) { try { const params = JSON.parse(paramsText || '{}'); const result = await this.bridge.rpc('LeviathanRunTool', [this.tool, params]); this.panel.webview.postMessage({ type: 'result', result }); } catch (err) { this.error(err); } } error(err) { this.panel.webview.postMessage({ type: 'error', error: err.message || String(err) }); } async loadHex(params = {}) { try { const start = Number(params.start || 0); const bytes = Number(params.bytes || 2048); const width = Number(params.width || 16); const result = await this.bridge.rpc('LeviathanRunTool', ['hex', { start, bytes, width }]); this.panel.webview.postMessage({ type: 'hexRows', rows: result.rows || [], start, bytes, width }); await this.refreshState(); } catch (err) { this.error(err); } } async setHexByte(msg) { try { const offset = Number(msg.offset); const value = String(msg.value || '').trim(); const payload = await this.bridge.rpc('LeviathanSetByte', [offset, value]); if (payload && payload.state) { this.fileState = payload.state; this.panel.webview.postMessage({ type: 'fileState', state: payload.state }); } await this.loadHex({ start: msg.start, bytes: msg.bytes, width: msg.width }); } catch (err) { this.panel.webview.postMessage({ type: 'hexEditError', offset: msg.offset, error: err.message || String(err) }); } } async searchHex(msg) { try { const mode = msg.mode === 'hex' ? 'hex' : 'text'; const query = String(msg.query || ''); const result = await this.bridge.rpc('LeviathanRunTool', ['search', { mode, query, max: 2000 }]); this.panel.webview.postMessage({ type: 'hexSearchResults', hits: result.hits || [], mode, query }); } catch (err) { this.error(err); } } async stateAction(method, msg = {}) { try { const state = await this.bridge.rpc(method); this.panel.webview.postMessage({ type: 'state', state }); this.panel.webview.postMessage({ type: 'fileState', state }); if (this.tool === 'hex') { await this.loadHex(msg.start === undefined ? DEFAULTS.hex : msg); } } catch (err) { this.error(err); } } hexHtml(meta) { const nonce = String(Date.now()); const initialFileState = JSON.stringify(this.fileState || null); const name = escapeHtml(meta.name); return `

${name}

Checking loaded file...
OffsetHexASCII
`; } html(meta) { const nonce = String(Date.now()); const defaults = JSON.stringify(DEFAULTS[this.tool] || {}); const initialFileState = JSON.stringify(this.fileState || null); const name = escapeHtml(meta.name); return `

${name}

Checking loaded file...
Parameters (JSON)
Adjust the parameters and select Run to see results.
`; } } async function loadFile(bridge, compare, view) { const picked = await vscode.window.showOpenDialog({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false }); if (!picked || !picked.length) return null; await bridge.rpc('LeviathanLoadFile', [picked[0].fsPath, compare]); if (view) await view.refreshState(); return bridge.rpc('LeviathanState'); } async function saveFile(bridge, view) { const state = await bridge.rpc('LeviathanState'); const defaultName = state && state.name ? `${state.name}.patched` : 'working-copy.bin'; const picked = await vscode.window.showSaveDialog({ defaultUri: vscode.Uri.file(defaultName) }); if (!picked) return; await bridge.rpc('LeviathanSaveWorkingToPath', [picked.fsPath]); await view.refreshState(); } async function ensureLoadedFile(bridge, toolsView) { try { const state = await bridge.rpc('LeviathanState'); if (state && state.name) return state; } catch (_) { // Fall through to picker. } const loaded = await loadFile(bridge, false, toolsView); if (!loaded || !loaded.name) { throw new Error('Load a file before opening a Leviathan tool.'); } return loaded; } async function openTool(context, bridge, toolsView, tool) { const state = await ensureLoadedFile(bridge, toolsView); const autoRun = !['case', 'patch-apply', 'spectrogram-encode'].includes(tool); new ToolPanel(context, bridge, tool, { autoRun, fileState: state }); } async function openSelectedTool(context, bridge, toolsView, tool, resource, selectedResources) { const target = Array.isArray(selectedResources) && selectedResources.length > 0 ? selectedResources[0] : resource; if (!target || target.scheme !== 'file') { vscode.window.showErrorMessage('Select a local file before opening it in Leviathan.'); return; } await bridge.rpc('LeviathanLoadFile', [target.fsPath, false]); await toolsView.refreshState(); const state = await bridge.rpc('LeviathanState'); const autoRun = !['case', 'patch-apply', 'spectrogram-encode'].includes(tool); new ToolPanel(context, bridge, tool, { autoRun, fileState: state }); } function escapeHtml(value) { return String(value).replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch])); } async function activate(context) { const bridge = new Bridge(context); await bridge.start(); const toolsView = new ToolsView(context, bridge); context.subscriptions.push(vscode.window.registerWebviewViewProvider('leviathan.tools', toolsView)); context.subscriptions.push(vscode.commands.registerCommand('leviathan.loadFile', () => loadFile(bridge, false, toolsView))); context.subscriptions.push(vscode.commands.registerCommand('leviathan.loadCompare', () => loadFile(bridge, true, toolsView))); context.subscriptions.push(vscode.commands.registerCommand('leviathan.saveFile', () => saveFile(bridge, toolsView))); context.subscriptions.push(vscode.commands.registerCommand('leviathan.openTool', tool => openTool(context, bridge, toolsView, tool))); for (const tool of TOOLS) { context.subscriptions.push(vscode.commands.registerCommand( `leviathan.openSelected.${tool.id}`, (resource, selectedResources) => openSelectedTool(context, bridge, toolsView, tool.id, resource, selectedResources) )); } } function deactivate() {} module.exports = { activate, deactivate };