feat(analysis): add global variables listing and table

This commit is contained in:
Rodrigo Verdiani 2026-06-10 09:46:47 -03:00
parent a2c3c1ced9
commit 56cbfb794d
6 changed files with 330 additions and 0 deletions

View File

@ -19,6 +19,7 @@ import type {
import type {
AnalysisResponse,
DataLabelResponse,
ImportResponse,
ListFunctionsParams,
PageFunctionSummaryResponse,
@ -580,6 +581,132 @@ export function useListImports<TData = Awaited<ReturnType<typeof listImports>>,
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<typeof customInstance>,
signal?: AbortSignal
) => {
return customInstance<DataLabelResponse[]>(
{
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<ReturnType<typeof listGlobalVars>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listGlobalVars>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
}
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListGlobalVarsQueryKey(analysisId);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listGlobalVars>>> = ({ signal }) =>
listGlobalVars(analysisId, requestOptions, signal);
return {
queryKey,
queryFn,
enabled: analysisId !== null && analysisId !== undefined,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof listGlobalVars>>, TError, TData> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
};
export type ListGlobalVarsQueryResult = NonNullable<Awaited<ReturnType<typeof listGlobalVars>>>;
export type ListGlobalVarsQueryError = unknown;
export function useListGlobalVars<
TData = Awaited<ReturnType<typeof listGlobalVars>>,
TError = unknown,
>(
analysisId: string,
options: {
query: Partial<UseQueryOptions<Awaited<ReturnType<typeof listGlobalVars>>, TError, TData>> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof listGlobalVars>>,
TError,
Awaited<ReturnType<typeof listGlobalVars>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListGlobalVars<
TData = Awaited<ReturnType<typeof listGlobalVars>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listGlobalVars>>, TError, TData>> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listGlobalVars>>,
TError,
Awaited<ReturnType<typeof listGlobalVars>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListGlobalVars<
TData = Awaited<ReturnType<typeof listGlobalVars>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listGlobalVars>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
/**
* @summary List global variables in an analysis
*/
export function useListGlobalVars<
TData = Awaited<ReturnType<typeof listGlobalVars>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listGlobalVars>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getListGlobalVarsQueryOptions(analysisId, options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* Retrieve paginated list of functions extracted during a static analysis.
* @summary List functions in an analysis

View File

@ -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;

View File

@ -152,6 +152,12 @@ export function CodeViewer({
</div>
</div>
{func?.signature && (
<div className="border-border shrink-0 border-b px-4 py-1">
<span className="text-muted-foreground font-mono text-xs">{func.signature}</span>
</div>
)}
{hasStackFrame && (
<div className="border-border shrink-0 border-b px-4 py-1">
<details>

View File

@ -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<string, string>;
onSelectFunction: (functionId: string) => void;
}
export function GlobalVarTable({
globalVars,
functionNameToId,
onSelectFunction,
}: GlobalVarTableProps) {
const [segmentFilter, setSegmentFilter] = useState("");
const [expandedIds, setExpandedIds] = useState<Set<string>>(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 (
<div className="flex min-h-0 flex-1 flex-col">
<div className="border-border shrink-0 border-b p-3">
<div className="relative">
<Search className="text-muted-foreground absolute top-1/2 left-2.5 h-4 w-4 -translate-y-1/2" />
<Input
value={segmentFilter}
onChange={(e) => setSegmentFilter(e.target.value)}
placeholder="Filter by segment..."
className="bg-muted h-8 border-transparent pl-9"
/>
</div>
</div>
<ScrollArea className="min-h-0 flex-1">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="w-[28px]" />
<TableHead>Name</TableHead>
<TableHead className="w-[110px]">Address</TableHead>
<TableHead className="w-[80px]">Segment</TableHead>
<TableHead className="w-[60px] text-right">Size</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((gv) => {
const hasXrefs = gv.dataXrefs && gv.dataXrefs.length > 0;
const expanded = !!(gv.id && expandedIds.has(gv.id));
return (
<Fragment key={gv.id}>
<TableRow
className="cursor-pointer"
onClick={() => gv.id && hasXrefs && toggleExpanded(gv.id)}
>
<TableCell className="px-1">
{hasXrefs &&
(expanded ? (
<ChevronDown className="text-muted-foreground h-3.5 w-3.5" />
) : (
<ChevronRight className="text-muted-foreground h-3.5 w-3.5" />
))}
</TableCell>
<TableCell className="font-mono text-xs">{gv.name}</TableCell>
<TableCell className="font-mono text-xs">{gv.address}</TableCell>
<TableCell className="text-xs">{gv.segment}</TableCell>
<TableCell className="text-right text-xs">{gv.size}</TableCell>
</TableRow>
{expanded && hasXrefs && (
<TableRow key={`xref-${gv.id}`}>
<TableCell colSpan={5} className="bg-muted/30 p-0">
<div className="px-9 py-2">
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="text-xs">Function</TableHead>
<TableHead className="w-[110px] text-xs">Address</TableHead>
<TableHead className="w-[80px] text-xs">Type</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{gv.dataXrefs!.map((xref, xi) => {
const funcId = xref.function
? functionNameToId[xref.function]
: undefined;
return (
<TableRow key={xi}>
<TableCell className="text-xs">
{funcId ? (
<button
type="button"
className="text-primary hover:underline"
onClick={(e) => {
e.stopPropagation();
onSelectFunction(funcId);
}}
>
{xref.function}
</button>
) : (
(xref.function ?? "")
)}
</TableCell>
<TableCell className="font-mono text-xs">
{xref.address}
</TableCell>
<TableCell className="text-xs">{xref.type}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</TableCell>
</TableRow>
)}
</Fragment>
);
})}
</TableBody>
</Table>
{filtered.length === 0 && (
<div className="text-muted-foreground py-4 text-center text-xs">
{segmentFilter ? "No matching global vars" : "No global vars found"}
</div>
)}
</ScrollArea>
<div className="border-border text-muted-foreground border-t p-3 text-xs">
{segmentFilter
? `${filtered.length} of ${globalVars.length} global vars`
: `${globalVars.length} global vars`}
</div>
</div>
);
}

View File

@ -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,

View File

@ -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 && (
<TabsTrigger value="imports">Imports</TabsTrigger>
)}
{globalVars && globalVars.length > 0 && (
<TabsTrigger value="global-vars">Global Vars</TabsTrigger>
)}
</TabsList>
<TabsContent
value="functions"
@ -176,6 +181,18 @@ const Binary = () => {
<ImportTable imports={imports} />
</TabsContent>
)}
{globalVars && globalVars.length > 0 && (
<TabsContent
value="global-vars"
className="mt-1 flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
>
<GlobalVarTable
globalVars={globalVars}
functionNameToId={functionNameToId}
onSelectFunction={setSelectedFunctionId}
/>
</TabsContent>
)}
</Tabs>
</div>
</ResizablePanel>