import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useExplorerSymbols, useExplorerFindings, } from '../api/queries/explorer'; import { useFinding } from '../api/queries/findings'; import { useDebugFunctions } from '../api/queries/debug'; import { ApiError } from '../api/client'; import { FileTree } from '../components/data-display/FileTree'; import { CodeViewer } from '../components/data-display/CodeViewer'; import { LoadingState } from '../components/ui/LoadingState'; import { usePageTitle } from '../hooks/usePageTitle'; import { EmptyState } from '../components/ui/EmptyState'; import { ExplorerIcon } from '../components/icons/Icons'; import { useFileTree } from '../hooks/useFileTree'; import { FunctionSelector } from './debug/FunctionSelector'; import { CfgAnalysisPanel } from './debug/CfgViewerPage'; import { SsaAnalysisPanel } from './debug/SsaViewerPage'; import { TaintAnalysisPanel } from './debug/TaintViewerPage'; import { SummaryAnalysisPanel } from './debug/SummaryExplorerPage'; import { AbstractInterpAnalysisPanel } from './debug/AbstractInterpPage'; import { SymexAnalysisPanel } from './debug/SymexPage'; import { PointerAnalysisPanel } from './debug/PointerViewerPage'; import { TypeFactsAnalysisPanel } from './debug/TypeFactsPage'; import { AuthAnalysisPanel } from './debug/AuthAnalysisPage'; import type { TreeEntry, FlowStep, FindingView } from '../api/types'; type ExplorerMode = 'tree' | 'symbols' | 'hotspots'; type ExplorerView = | 'code' | 'cfg' | 'ssa' | 'taint' | 'summaries' | 'abstract-interp' | 'symex' | 'pointer' | 'type-facts' | 'auth'; const FLOW_KIND_COLORS: Record = { source: 'var(--success)', assignment: 'var(--accent)', call: 'var(--sev-medium)', phi: 'var(--text-tertiary)', sink: 'var(--sev-high)', }; const FLOW_KIND_LABELS: Record = { source: 'Source', assignment: 'Assign', call: 'Call', phi: 'Phi', sink: 'Sink', }; const VIEW_CONFIG: Array<{ id: ExplorerView; label: string; requiresFunction?: boolean; supportsFunction?: boolean; }> = [ { id: 'code', label: 'Code' }, { id: 'cfg', label: 'CFG', requiresFunction: true, supportsFunction: true }, { id: 'ssa', label: 'SSA', requiresFunction: true, supportsFunction: true }, { id: 'taint', label: 'Taint', requiresFunction: true, supportsFunction: true, }, { id: 'summaries', label: 'Summaries', supportsFunction: true }, { id: 'abstract-interp', label: 'Abstract Interp', requiresFunction: true, supportsFunction: true, }, { id: 'symex', label: 'Symex', requiresFunction: true, supportsFunction: true, }, { id: 'pointer', label: 'Pointer', requiresFunction: true, supportsFunction: true, }, { id: 'type-facts', label: 'Type Facts', requiresFunction: true, supportsFunction: true, }, { id: 'auth', label: 'Auth' }, ]; const VIEW_CONFIG_BY_ID = new Map(VIEW_CONFIG.map((view) => [view.id, view])); export function ExplorerPage() { usePageTitle('Explorer'); const [params, setParams] = useSearchParams(); const [explorerMode, setExplorerMode] = useState('tree'); const [showClosures, setShowClosures] = useState(false); const [highlightLine, setHighlightLine] = useState(); const [selectedFindingIndex, setSelectedFindingIndex] = useState< number | null >(null); const [invalidFunctionNotice, setInvalidFunctionNotice] = useState< string | null >(null); const codeScrollPositionsRef = useRef>({}); const rawView = params.get('view'); const rawFile = params.get('file') || null; const rawFunction = params.get('function') || null; const currentView: ExplorerView = isExplorerView(rawView) ? rawView : 'code'; const currentViewConfig = VIEW_CONFIG_BY_ID.get(currentView)!; const isCodeView = currentView === 'code'; const updateExplorerParams = useCallback( ( updates: Partial>, replace = false, ) => { setParams( (prev) => { const next = new URLSearchParams(prev); for (const [key, value] of Object.entries(updates)) { if (value) { next.set(key, value); } else { next.delete(key); } } return next; }, { replace }, ); }, [setParams], ); useEffect(() => { if (rawView !== currentView) { updateExplorerParams({ view: currentView }, true); } }, [currentView, rawView, updateExplorerParams]); const { data: symbolEntries, error: symbolsError } = useExplorerSymbols(rawFile); const closureSymbolCount = useMemo( () => symbolEntries?.filter((s) => s.func_kind === 'closure').length ?? 0, [symbolEntries], ); const visibleSymbolEntries = useMemo(() => { if (!symbolEntries) return symbolEntries; return showClosures ? symbolEntries : symbolEntries.filter((s) => s.func_kind !== 'closure'); }, [symbolEntries, showClosures]); const hasInvalidFile = Boolean( rawFile && isPathResolutionError(symbolsError), ); const hasFileLookupError = Boolean( rawFile && symbolsError && !hasInvalidFile, ); const selectedFile = rawFile && !hasInvalidFile ? rawFile : null; const handleFileSelect = useCallback( (path: string) => { setHighlightLine(undefined); setSelectedFindingIndex(null); setInvalidFunctionNotice(null); updateExplorerParams({ file: path, function: null }); }, [updateExplorerParams], ); const { rootEntries, isLoading: treeLoading, expandedPaths, loadedChildren, selectedPath, handleToggleExpand, handleSelectFile, } = useFileTree(selectedFile, handleFileSelect); const { data: functions, isLoading: functionsLoading } = useDebugFunctions(selectedFile); const selectedFunction = rawFunction && functions?.some((fn) => fn.name === rawFunction) ? rawFunction : null; const hasFunctionOptions = (functions?.length ?? 0) > 0; useEffect(() => { if (!rawFunction) { return; } if (!selectedFile) { setInvalidFunctionNotice( `Function "${rawFunction}" was cleared because no valid file is selected.`, ); updateExplorerParams({ function: null }, true); return; } if (!functions) { return; } if (!functions.some((fn) => fn.name === rawFunction)) { setInvalidFunctionNotice( `Function "${rawFunction}" was not found in ${selectedFile}.`, ); updateExplorerParams({ function: null }, true); } }, [functions, rawFunction, selectedFile, updateExplorerParams]); const { data: findings } = useExplorerFindings(selectedFile); const { data: fullFinding } = useFinding(selectedFindingIndex ?? ''); const handleSelectFinding = useCallback((index: number, line: number) => { setSelectedFindingIndex(index); setHighlightLine(line); }, []); const handleViewSelect = useCallback( (view: ExplorerView) => { updateExplorerParams({ view }); }, [updateExplorerParams], ); const handleFunctionChange = useCallback( (fnName: string | null) => { setInvalidFunctionNotice(null); updateExplorerParams({ function: fnName }); }, [updateExplorerParams], ); const selectedEntry = findEntry(rootEntries, loadedChildren, selectedFile); const language = selectedEntry?.language || ''; const hotspotFiles = useMemo( () => buildHotspotList(rootEntries, loadedChildren), [loadedChildren, rootEntries], ); const sevBreakdown = findings ? findings.reduce( (acc, finding) => { const key = finding.severity.toUpperCase(); acc[key] = (acc[key] || 0) + 1; return acc; }, {} as Record, ) : {}; const evidence = fullFinding?.evidence; const flowSteps = evidence?.flow_steps; const hasFlow = flowSteps && flowSteps.length > 0; const hasStateEvidence = fullFinding?.rule_id.startsWith('state-') && evidence?.state; const codeHighlights = selectedFindingIndex != null && evidence ? { sourceLine: evidence.source?.line, sinkLine: evidence.sink?.line, findingLine: fullFinding?.line, } : undefined; const flowLineSet = new Set(); if (hasFlow) { for (const step of flowSteps) { if (step.line) { flowLineSet.add(step.line); } } } const analysisContent = renderAnalysisContent({ currentView, currentViewLabel: currentViewConfig.label, selectedFile, selectedFunction, functions, functionsLoading, onBrowseFiles: () => handleViewSelect('code'), }); return (
{(['tree', 'symbols', 'hotspots'] as ExplorerMode[]).map((mode) => ( ))}
{explorerMode === 'tree' && ( <> {treeLoading && } {rootEntries && ( )} )} {explorerMode === 'symbols' && (
{!selectedFile && (
Select a file to view symbols
)} {selectedFile && symbolEntries && symbolEntries.length === 0 && (
No symbols found
)} {selectedFile && closureSymbolCount > 0 && ( )} {selectedFile && visibleSymbolEntries?.map((sym, index) => (
{sym.kind === 'function' ? 'ƒ' : 'm'} {sym.name} {sym.arity !== undefined && sym.arity !== null && ( ({sym.arity}) )} {sym.func_kind === 'closure' && ( {sym.container ? `[closure in ${sym.container}]` : '[closure]'} )} {sym.finding_count > 0 && ( {sym.finding_count} )}
))}
)} {explorerMode === 'hotspots' && (
{hotspotFiles.length === 0 && (
No findings in scanned files
)} {hotspotFiles.map((entry) => (
handleSelectFile(entry.path)} > {entry.name} {entry.finding_count}
))}
)}
File {selectedFile || 'Select a file in Explorer'}
{selectedFile && currentViewConfig.supportsFunction && ( )}
{VIEW_CONFIG.map((view) => ( ))}
{hasInvalidFile && rawFile && (
The requested file {rawFile} could not be found. Choose another file in Explorer.
)} {hasFileLookupError && (
Explorer could not validate the selected file right now.
)} {invalidFunctionNotice && (
{invalidFunctionNotice}
)}
{isCodeView ? ( <> {!selectedFile && ( } message={ hasInvalidFile ? 'Choose a file from the Explorer to continue.' : 'Select a file from the tree to view its contents.' } /> )} {selectedFile && ( 0 ? flowLineSet : undefined} language={language} initialScrollTop={ codeScrollPositionsRef.current[selectedFile] } onScrollPositionChange={(scrollTop) => { codeScrollPositionsRef.current[selectedFile] = scrollTop; }} /> )} ) : ( analysisContent )}
{isCodeView && (
{!selectedFile && (
Select a file to view analysis details
)} {selectedFile && ( <>

File Summary

{language && {language}} {findings ? findings.length : 0} finding {findings?.length !== 1 ? 's' : ''}
{findings && findings.length > 0 && (
{Object.entries(sevBreakdown) .sort(([a], [b]) => sevOrder(a) - sevOrder(b)) .map(([sev, count]) => ( {sev}: {count} ))}
)}

Symbols

{symbolEntries && symbolEntries.length === 0 && (
No symbols found
)} {visibleSymbolEntries?.map((sym, index) => (
{sym.kind === 'function' ? 'ƒ' : 'm'} {sym.name} {sym.func_kind === 'closure' && ( [closure] )}
))} {!showClosures && closureSymbolCount > 0 && ( )}

Findings

{findings && findings.length === 0 && (
No findings in this file
)}
{findings?.map((finding) => (
handleSelectFinding(finding.index, finding.line) } > L{finding.line} {finding.rule_id} {finding.message && ( {finding.message} )}
))}
{hasFlow && (

Taint Flow

setHighlightLine(line)} />
)} {hasStateEvidence && fullFinding && ( )} )}
)}
); } function renderAnalysisContent({ currentView, currentViewLabel, selectedFile, selectedFunction, functions, functionsLoading, onBrowseFiles, }: { currentView: ExplorerView; currentViewLabel: string; selectedFile: string | null; selectedFunction: string | null; functions: Array<{ name: string }> | undefined; functionsLoading: boolean; onBrowseFiles: () => void; }) { if (!selectedFile) { return ( } message="Select a file from the tree to view its contents." /> ); } if (currentView === 'summaries') { return (
); } if (currentView === 'auth') { return (
); } if (functionsLoading) { return ; } if ((functions?.length ?? 0) === 0) { return ( ); } if (!selectedFunction) { return ( ); } switch (currentView) { case 'cfg': return ( ); case 'ssa': return (
); case 'taint': return (
); case 'abstract-interp': return (
); case 'symex': return (
); case 'pointer': return (
); case 'type-facts': return (
); case 'code': return null; } } function AnalysisEmptyState({ title, message, onBrowseFiles, }: { title: string; message: string; onBrowseFiles?: () => void; }) { return (

{title}

{message}

{onBrowseFiles && ( )}
); } function ExplorerFlowTimeline({ steps, onStepClick, }: { steps: FlowStep[]; onStepClick: (line: number) => void; }) { return (
{steps.map((step, index) => { const color = FLOW_KIND_COLORS[step.kind] || 'var(--text-secondary)'; const label = FLOW_KIND_LABELS[step.kind] || step.kind; const isLast = index === steps.length - 1; return (
step.line && onStepClick(step.line)} >
{!isLast &&
}
{label} {step.variable && ( {step.variable} )} {step.callee && ( {step.callee} )}
L{step.line}:{step.col} {step.function ? ` in ${step.function}` : ''}
{step.snippet && (
{step.snippet}
)}
); })}
); } const STATE_REMEDIATION_HINTS: Record = { 'state-use-after-close': 'Ensure the resource is not accessed after calling close/free.', 'state-double-close': 'Remove the duplicate close call, or guard with a null/closed check.', 'state-resource-leak': 'Add a close/free call before the function exits, or use defer/with/try-with-resources/RAII.', 'state-resource-leak-possible': 'Ensure the resource is closed on all code paths, including error/early-return paths.', 'state-unauthed-access': 'Add an authentication check before this operation, or move it behind auth middleware.', }; function ExplorerStateDetail({ finding }: { finding: FindingView }) { const state = finding.evidence?.state; if (!state) { return null; } const isAuth = state.machine === 'auth'; const machineLabel = isAuth ? 'Authentication State' : 'Resource Lifecycle'; const hint = STATE_REMEDIATION_HINTS[finding.rule_id]; const acquireLocation = finding.rule_id.includes('leak') && finding.evidence?.sink ? `L${finding.evidence.sink.line}:${finding.evidence.sink.col}` : null; return (

State Analysis

{machineLabel}
{state.subject && (
Variable: {state.subject}
)}
{state.from_state} {state.to_state}
{acquireLocation && (
Acquired at: {acquireLocation}
)}
{hint && (
Remediation
{hint}
)}
); } function findEntry( rootEntries: TreeEntry[] | undefined, loadedChildren: Map, path: string | null, ): TreeEntry | undefined { if (!path) { return undefined; } if (rootEntries) { const found = rootEntries.find((entry) => entry.path === path); if (found) { return found; } } for (const children of loadedChildren.values()) { const found = children.find((entry) => entry.path === path); if (found) { return found; } } return undefined; } function buildHotspotList( rootEntries: TreeEntry[] | undefined, loadedChildren: Map, ): TreeEntry[] { const files: TreeEntry[] = []; function collect(entries: TreeEntry[]) { for (const entry of entries) { if (entry.entry_type === 'file' && entry.finding_count > 0) { files.push(entry); } if (entry.entry_type === 'dir') { const children = loadedChildren.get(entry.path); if (children) { collect(children); } } } } if (rootEntries) { collect(rootEntries); } files.sort((a, b) => b.finding_count - a.finding_count); return files; } function sevOrder(sev: string): number { switch (sev) { case 'HIGH': return 0; case 'MEDIUM': return 1; case 'LOW': return 2; default: return 3; } } function isExplorerView(value: string | null): value is ExplorerView { return VIEW_CONFIG_BY_ID.has(value as ExplorerView); } function isPathResolutionError(error: unknown): boolean { return ( error instanceof ApiError && (error.status === 403 || error.status === 404) ); }