'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...
`;
}
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...
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 };