feat(analysis): add global variables listing and table
This commit is contained in:
parent
a2c3c1ced9
commit
56cbfb794d
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
158
src/features/binary/GlobalVarTable.tsx
Normal file
158
src/features/binary/GlobalVarTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user