From 56cbfb794de3c6f357146412c8bccc1aa2fb1064 Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Wed, 10 Jun 2026 09:46:47 -0300 Subject: [PATCH] feat(analysis): add global variables listing and table --- src/api/generated/analysis/analysis.ts | 127 +++++++++++++++++ src/api/generated/api.schemas.ts | 15 ++ src/features/binary/CodeViewer.tsx | 6 + src/features/binary/GlobalVarTable.tsx | 158 +++++++++++++++++++++ src/features/binary/hooks/useBinaryPage.ts | 7 + src/pages/Binary.tsx | 17 +++ 6 files changed, 330 insertions(+) create mode 100644 src/features/binary/GlobalVarTable.tsx diff --git a/src/api/generated/analysis/analysis.ts b/src/api/generated/analysis/analysis.ts index 146449b..93ebf40 100644 --- a/src/api/generated/analysis/analysis.ts +++ b/src/api/generated/analysis/analysis.ts @@ -19,6 +19,7 @@ import type { import type { AnalysisResponse, + DataLabelResponse, ImportResponse, ListFunctionsParams, PageFunctionSummaryResponse, @@ -580,6 +581,132 @@ export function useListImports>, return { ...query, queryKey: queryOptions.queryKey }; } +/** + * Retrieve all named data labels (global variables) identified during a static analysis. + * @summary List global variables in an analysis + */ +export const listGlobalVars = ( + analysisId: string, + options?: SecondParameter, + signal?: AbortSignal +) => { + return customInstance( + { + url: `/analysis/${encodeURIComponent(String(analysisId))}/global-vars`, + method: "GET", + signal, + }, + options + ); +}; + +export const getListGlobalVarsQueryKey = (analysisId: string) => { + return [`/analysis/${analysisId}/global-vars`] as const; +}; + +export const getListGlobalVarsQueryOptions = < + TData = Awaited>, + TError = unknown, +>( + analysisId: string, + options?: { + query?: Partial>, TError, TData>>; + request?: SecondParameter; + } +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getListGlobalVarsQueryKey(analysisId); + + const queryFn: QueryFunction>> = ({ signal }) => + listGlobalVars(analysisId, requestOptions, signal); + + return { + queryKey, + queryFn, + enabled: analysisId !== null && analysisId !== undefined, + ...queryOptions, + } as UseQueryOptions>, TError, TData> & { + queryKey: DataTag; + }; +}; + +export type ListGlobalVarsQueryResult = NonNullable>>; +export type ListGlobalVarsQueryError = unknown; + +export function useListGlobalVars< + TData = Awaited>, + TError = unknown, +>( + analysisId: string, + options: { + query: Partial>, TError, TData>> & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): DefinedUseQueryResult & { queryKey: DataTag }; +export function useListGlobalVars< + TData = Awaited>, + TError = unknown, +>( + analysisId: string, + options?: { + query?: Partial>, TError, TData>> & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +export function useListGlobalVars< + TData = Awaited>, + TError = unknown, +>( + analysisId: string, + options?: { + query?: Partial>, TError, TData>>; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +/** + * @summary List global variables in an analysis + */ + +export function useListGlobalVars< + TData = Awaited>, + TError = unknown, +>( + analysisId: string, + options?: { + query?: Partial>, TError, TData>>; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag } { + const queryOptions = getListGlobalVarsQueryOptions(analysisId, options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { + queryKey: DataTag; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + /** * Retrieve paginated list of functions extracted during a static analysis. * @summary List functions in an analysis diff --git a/src/api/generated/api.schemas.ts b/src/api/generated/api.schemas.ts index 8cbd264..2671270 100644 --- a/src/api/generated/api.schemas.ts +++ b/src/api/generated/api.schemas.ts @@ -199,6 +199,21 @@ export interface ImportResponse { ordinal?: number; } +export interface DataXrefResponse { + function?: string; + address?: string; + type?: string; +} + +export interface DataLabelResponse { + id?: string; + name?: string; + address?: string; + segment?: string; + size?: number; + dataXrefs?: DataXrefResponse[]; +} + export interface Pageable { /** @minimum 0 */ page?: number; diff --git a/src/features/binary/CodeViewer.tsx b/src/features/binary/CodeViewer.tsx index e07d9a5..bf80dd1 100644 --- a/src/features/binary/CodeViewer.tsx +++ b/src/features/binary/CodeViewer.tsx @@ -152,6 +152,12 @@ export function CodeViewer({ + {func?.signature && ( +
+ {func.signature} +
+ )} + {hasStackFrame && (
diff --git a/src/features/binary/GlobalVarTable.tsx b/src/features/binary/GlobalVarTable.tsx new file mode 100644 index 0000000..a25066c --- /dev/null +++ b/src/features/binary/GlobalVarTable.tsx @@ -0,0 +1,158 @@ +import { useState, Fragment } from "react"; +import { Search, ChevronRight, ChevronDown } from "lucide-react"; +import type { DataLabelResponse } from "@/api/generated/api.schemas"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +interface GlobalVarTableProps { + globalVars: DataLabelResponse[]; + functionNameToId: Record; + onSelectFunction: (functionId: string) => void; +} + +export function GlobalVarTable({ + globalVars, + functionNameToId, + onSelectFunction, +}: GlobalVarTableProps) { + const [segmentFilter, setSegmentFilter] = useState(""); + const [expandedIds, setExpandedIds] = useState>(new Set()); + + const filtered = globalVars.filter( + (gv) => !segmentFilter || gv.segment?.toLowerCase().includes(segmentFilter.toLowerCase()) + ); + + const toggleExpanded = (id: string) => { + setExpandedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }; + + return ( +
+
+
+ + setSegmentFilter(e.target.value)} + placeholder="Filter by segment..." + className="bg-muted h-8 border-transparent pl-9" + /> +
+
+ + + + + + + Name + Address + Segment + Size + + + + {filtered.map((gv) => { + const hasXrefs = gv.dataXrefs && gv.dataXrefs.length > 0; + const expanded = !!(gv.id && expandedIds.has(gv.id)); + + return ( + + gv.id && hasXrefs && toggleExpanded(gv.id)} + > + + {hasXrefs && + (expanded ? ( + + ) : ( + + ))} + + {gv.name} + {gv.address} + {gv.segment} + {gv.size} + + {expanded && hasXrefs && ( + + +
+
+ + + Function + Address + Type + + + + {gv.dataXrefs!.map((xref, xi) => { + const funcId = xref.function + ? functionNameToId[xref.function] + : undefined; + return ( + + + {funcId ? ( + + ) : ( + (xref.function ?? "") + )} + + + {xref.address} + + {xref.type} + + ); + })} + +
+
+ + + )} + + ); + })} + + + {filtered.length === 0 && ( +
+ {segmentFilter ? "No matching global vars" : "No global vars found"} +
+ )} + + +
+ {segmentFilter + ? `${filtered.length} of ${globalVars.length} global vars` + : `${globalVars.length} global vars`} +
+
+ ); +} diff --git a/src/features/binary/hooks/useBinaryPage.ts b/src/features/binary/hooks/useBinaryPage.ts index 43e2249..b003b32 100644 --- a/src/features/binary/hooks/useBinaryPage.ts +++ b/src/features/binary/hooks/useBinaryPage.ts @@ -9,6 +9,7 @@ import { useListSegments, useListStrings, useListImports, + useListGlobalVars, } from "@/api/generated/analysis/analysis.ts"; import { useGetFunction } from "@/api/generated/function/function.ts"; import { useListEngines } from "@/api/generated/engine/engine.ts"; @@ -91,6 +92,11 @@ export function useBinaryPage() { query: { enabled: !!analysisId }, }); + // Fetch global vars for the analysis + const { data: globalVars } = useListGlobalVars(analysisId ?? "", { + query: { enabled: !!analysisId }, + }); + // Infinite query for functions pagination — single page with all functions const functionsQuery = useInfiniteQuery({ queryKey: ["functions", analysisId], @@ -250,6 +256,7 @@ export function useBinaryPage() { functionsTotal: functionsQuery.data?.pages[0]?.totalElements ?? 0, strings: stringsList, imports: imports ?? [], + globalVars: globalVars ?? [], functionNameToId, functionStrings, selectedFunction, diff --git a/src/pages/Binary.tsx b/src/pages/Binary.tsx index 39e2e61..d9e43ea 100644 --- a/src/pages/Binary.tsx +++ b/src/pages/Binary.tsx @@ -7,6 +7,7 @@ import { BinaryHeader } from "@/features/binary/BinaryHeader.tsx"; import { FunctionTree } from "@/features/binary/FunctionTree"; import { StringTable } from "@/features/binary/StringTable"; import { ImportTable } from "@/features/binary/ImportTable"; +import { GlobalVarTable } from "@/features/binary/GlobalVarTable"; import { XrefPanel } from "@/features/binary/XrefPanel"; import { ResizableHandle, @@ -53,6 +54,7 @@ const Binary = () => { engines, enginesLoading, imports, + globalVars, } = useBinaryPage(); const navigateToEntryPoint = () => { @@ -136,6 +138,9 @@ const Binary = () => { {imports && imports.length > 0 && ( Imports )} + {globalVars && globalVars.length > 0 && ( + Global Vars + )} { )} + {globalVars && globalVars.length > 0 && ( + + + + )}