Z-Hound Reforged

Filters:
Quick View:
Layout: Labels:
Click any node to inspect it
Upload a SharpHound ZIP or JSON file to begin
zrnge
Legend
User
Group
Computer
Domain
GPO/OU
Cert
High Value
DCSync
Azure:
AZ User
AZ Group
AZ App
AZ SP
AZ VM
AZ Role
AZ Tenant
\n`; const blob = new Blob([fullHtml], { type: 'text/html;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `zhound-report-${new Date().toISOString().slice(0,10)}.html`; a.click(); URL.revokeObjectURL(url); } function clearGraph() { if (cy) { cy.destroy(); cy = null; } adNodes = {}; adEdges = []; foundPaths = []; ownedNodes = new Set(); loadErrors = []; unknownEdgeTypes = {}; sidIndex = {}; nameToSid = {}; gpoNames = {}; activeFilters = { excludeTypes: new Set(), excludeDomains: new Set(), includeOUs: new Set(), includeGroups: new Set() }; egoNetworkNode = null; _pathsPage = 0; _riskExpanded.clear(); Object.keys(_edgeSecPages).forEach(k => delete _edgeSecPages[k]); document.getElementById('zipInput').value = ''; document.getElementById('searchInput').value = ''; document.getElementById('quickView').innerHTML = ''; document.getElementById('statsBar').classList.add('hidden'); document.getElementById('exitFocusBtn').classList.add('hidden'); document.getElementById('statusMessage').textContent = 'Upload a SharpHound ZIP or JSON file to begin'; document.getElementById('pane-details').innerHTML = '
Click any node to inspect it
'; document.getElementById('pane-paths').innerHTML = '
Use "Find DA Path" or "All Paths" above
'; document.getElementById('pane-stats').innerHTML = '
Upload data to see risk report
'; } // ================================================================ // FILE UPLOAD // ================================================================ function showLoadSummary() { renderRiskReport(); if (loadErrors.length > 0 || Object.keys(unknownEdgeTypes).length > 0) { switchTab('stats'); } } document.getElementById('zipInput').addEventListener('change', async function(e) { const files = Array.from(e.target.files); if (!files.length) return; adNodes = {}; adEdges = []; foundPaths = []; loadErrors = []; unknownEdgeTypes = {}; if (cy) { cy.destroy(); cy = null; } const MAX_UNCOMPRESSED = 500 * 1024 * 1024; // 500 MB const MAX_NODES = 100000; let totalUncompressed = 0; let aborted = false; const statusEl = document.getElementById('statusMessage'); // First pass: collect all JSON entries to parse (also validates ZIPs) const filesToParse = []; // { filename, getText: async fn } for (let fi = 0; fi < files.length && !aborted; fi++) { const file = files[fi]; statusEl.textContent = `Reading ${file.name} (${fi + 1}/${files.length})...`; const nameLower = file.name.toLowerCase(); if (nameLower.endsWith('.zip')) { let zip; try { zip = await JSZip.loadAsync(file); } catch (err) { loadErrors.push({ filename: file.name, error: 'ZIP read error', reason: err.message }); continue; } for (const [filename, zipEntry] of Object.entries(zip.files)) { if (!filename.toLowerCase().endsWith('.json') || zipEntry.dir) continue; const uncSize = (zipEntry._data && zipEntry._data.uncompressedSize) || 0; totalUncompressed += uncSize; if (totalUncompressed > MAX_UNCOMPRESSED) { aborted = true; const reason = 'Uncompressed content exceeds 500 MB limit. Load aborted to protect the browser.'; loadErrors.push({ filename: file.name + ' / ' + filename, error: 'ZIP bomb protection', reason }); statusEl.textContent = reason; break; } filesToParse.push({ filename, getText: () => zipEntry.async('string') }); } } else if (nameLower.endsWith('.json')) { totalUncompressed += file.size; if (totalUncompressed > MAX_UNCOMPRESSED) { aborted = true; const reason = 'Total input size exceeds 500 MB limit. Load aborted.'; loadErrors.push({ filename: file.name, error: 'Size limit exceeded', reason }); statusEl.textContent = reason; break; } filesToParse.push({ filename: file.name, getText: () => file.text() }); } else { loadErrors.push({ filename: file.name, error: 'Unsupported format', reason: 'Only .zip and .json files are accepted' }); } } if (aborted) { showLoadSummary(); return; } // Second pass: read and parse each JSON file for (let i = 0; i < filesToParse.length && !aborted; i++) { const { filename, getText } = filesToParse[i]; statusEl.textContent = `Processing ${filename} (${i + 1}/${filesToParse.length})...`; let text; try { text = await getText(); } catch (err) { loadErrors.push({ filename, error: 'Read error', reason: err.message }); continue; } // Strip UTF-8 BOM (U+FEFF) that SharpHound prepends to its JSON files text = text.replace(/^\uFEFF/, ''); if (!text || !text.trim()) { loadErrors.push({ filename, error: 'Empty file', reason: 'File contains no data' }); continue; } let json; try { json = JSON.parse(text); } catch (err) { loadErrors.push({ filename, error: 'Invalid JSON', reason: err.message.slice(0, 150) }); continue; } try { parseFile(json, filename); } catch (err) { loadErrors.push({ filename, error: 'Parse error', reason: err.message.slice(0, 150) }); continue; } if (Object.keys(adNodes).length > MAX_NODES) { aborted = true; const reason = `Node count exceeded 100,000 after processing ${filename}. Load aborted to prevent browser hang.`; loadErrors.push({ filename, error: 'Node limit exceeded', reason }); statusEl.textContent = reason; break; } } if (Object.keys(adNodes).length === 0) { statusEl.textContent = loadErrors.length ? `No objects loaded — ${loadErrors.length} file(s) had errors. See Risk Report tab.` : 'No objects found in the uploaded files.'; showLoadSummary(); return; } resolveEdges(); resolveWellKnownSIDs(); buildSidIndex(); detectDCSync(); computeStats(); buildQuickViews(); buildFilterPanel(); renderRiskReport(); const total = Object.keys(adNodes).length; const warnCount = loadErrors.length + Object.keys(unknownEdgeTypes).length; statusEl.textContent = `Loaded ${total} objects, ${adEdges.length} edges.${warnCount ? ` ${warnCount} warning(s) — see Risk Report.` : ''} Rendering...`; setTimeout(() => { try { renderGraph(); } catch (err) { statusEl.textContent = `Render failed: ${err.message} — ${total} nodes and ${adEdges.length} edges were loaded.`; console.error('renderGraph error:', err); } }, 80); }); // ================================================================ // RIGHT-CLICK CONTEXT MENU // ================================================================ let _ctxNodeId = null; function showCtxMenu(x, y, nodeId) { _ctxNodeId = nodeId; const menu = document.getElementById('ctxMenu'); const n = adNodes[nodeId]; const ctxLabel = n ? (n.displayName || n.name).split('@')[0].split('\\').pop() : nodeId; document.getElementById('ctxNodeName').textContent = ctxLabel; menu.classList.remove('hidden'); // x/y are clientX/clientY (viewport coords); menu is absolute inside the graph panel const container = document.getElementById('cy').parentElement; const rect = container.getBoundingClientRect(); let lx = x - rect.left; let ly = y - rect.top; // Keep menu inside the container const mw = 176, mh = 220; if (lx + mw > rect.width) lx = rect.width - mw - 4; if (ly + mh > rect.height) ly = rect.height - mh - 4; menu.style.left = Math.max(0, lx) + 'px'; menu.style.top = Math.max(0, ly) + 'px'; } function hideCtxMenu() { document.getElementById('ctxMenu').classList.add('hidden'); _ctxNodeId = null; } document.addEventListener('click', e => { if (!e.target.closest('#ctxMenu')) hideCtxMenu(); }); document.addEventListener('keydown', e => { if (e.key === 'Escape') hideCtxMenu(); }); function ctxCopyName() { if (!_ctxNodeId) return; const n = adNodes[_ctxNodeId]; navigator.clipboard.writeText(n ? (n.displayName || n.name) : _ctxNodeId).catch(() => {}); hideCtxMenu(); } function ctxToggleOwned() { if (_ctxNodeId) toggleOwned(_ctxNodeId); hideCtxMenu(); } function ctxFindPath() { if (!_ctxNodeId) return; const daSet = new Set(Object.keys(adNodes).filter(id => adNodes[id].isAdmin)); const result = bfsPath(_ctxNodeId, daSet); foundPaths = result ? [{ from: _ctxNodeId, path: result, hops: result.nodes.length - 1 }] : []; _pathsPage = 0; document.getElementById('statPaths').textContent = foundPaths.length; renderPathsPanel(); switchTab('paths'); document.getElementById('statusMessage').textContent = ''; hideCtxMenu(); } function ctxShowDetails() { if (_ctxNodeId) showNodeDetails(_ctxNodeId); hideCtxMenu(); } function ctxExpandNeighbors() { if (!_ctxNodeId || !cy) { hideCtxMenu(); return; } const el = cy.getElementById(_ctxNodeId); if (!el.length) { hideCtxMenu(); return; } cy.nodes().style({ opacity: 0.12 }); cy.edges().style({ opacity: 0.04 }); el.closedNeighborhood().style({ opacity: 1 }); el.connectedEdges().style({ opacity: 0.7 }); hideCtxMenu(); } // ================================================================ // SAVE / LOAD SESSION (localStorage) // ================================================================ function saveSession() { if (!Object.keys(adNodes).length) { alert('No data to save.'); return; } try { const payload = JSON.stringify({ v: 1, nodes: adNodes, edges: adEdges, owned: [...ownedNodes] }); localStorage.setItem('zhound_session', payload); const kb = Math.round(payload.length / 1024); document.getElementById('statusMessage').textContent = `Session saved (${kb} KB). Use Load to restore.`; } catch (e) { alert('Session too large for browser storage — use Export CSV to save data externally.'); } } function loadSession() { const raw = localStorage.getItem('zhound_session'); if (!raw) { alert('No saved session found. Use Save first.'); return; } try { const data = JSON.parse(raw); if (!data.nodes || !data.edges) throw new Error('Invalid session format'); adNodes = data.nodes; adEdges = data.edges; ownedNodes = new Set(data.owned || []); loadErrors = []; unknownEdgeTypes = {}; computeStats(); buildQuickViews(); buildFilterPanel(); renderRiskReport(); renderGraph(); updateOwnedBadge(); // Re-apply owned styling after graph is rendered setTimeout(() => { ownedNodes.forEach(id => { const el = cy?.getElementById(id); if (el?.length) { el.style('border-width', 4); el.style('border-color', '#00ff88'); el.style('border-style', 'dashed'); } }); }, 300); document.getElementById('statusMessage').textContent = ''; } catch (e) { alert('Failed to load session: ' + e.message); } } // ================================================================ // ADDITIONAL QUICK QUERIES // ================================================================ // shadow_admin / stale / golden_ticket are added to runQuery below // via extending the existing else-if chain // ================================================================ // RESIZABLE PANEL // ================================================================ const resizer = document.getElementById('resizer'); const leftPanel = document.getElementById('leftPanel'); let isResizing = false; resizer.addEventListener('mousedown', () => { isResizing = true; document.body.style.cursor = 'col-resize'; document.body.style.userSelect = 'none'; }); document.addEventListener('mousemove', e => { if (!isResizing) return; const w = Math.max(180, Math.min(e.clientX, 600)); leftPanel.style.width = `${w}px`; }); document.addEventListener('mouseup', () => { if (!isResizing) return; isResizing = false; document.body.style.cursor = ''; document.body.style.userSelect = ''; if (cy) { cy.resize(); cy.fit(); } }); window.addEventListener('resize', () => { if (cy) { cy.resize(); cy.fit(); } }); // Register dagre layout if (typeof cytoscapeDagre !== 'undefined') cytoscape.use(cytoscapeDagre);