feat/ida66 #3

Merged
rov merged 6 commits from feat/ida66 into main 2026-06-10 11:57:14 -03:00
8 changed files with 387 additions and 33 deletions
Showing only changes of commit 908d57c3c4 - Show all commits

View File

@ -22,6 +22,7 @@ import type {
ListFunctionsParams,
PageFunctionSummaryResponse,
SegmentResponse,
StringResponse,
} from "../api.schemas";
import { customInstance } from "../../api";
@ -248,6 +249,116 @@ export function useGetAnalysis<TData = Awaited<ReturnType<typeof getAnalysis>>,
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* Retrieve all strings extracted during a static analysis.
* @summary List strings in an analysis
*/
export const listStrings = (
analysisId: string,
options?: SecondParameter<typeof customInstance>,
signal?: AbortSignal
) => {
return customInstance<StringResponse[]>(
{ url: `/analysis/${encodeURIComponent(String(analysisId))}/strings`, method: "GET", signal },
options
);
};
export const getListStringsQueryKey = (analysisId: string) => {
return [`/analysis/${analysisId}/strings`] as const;
};
export const getListStringsQueryOptions = <
TData = Awaited<ReturnType<typeof listStrings>>,
TError = unknown,
>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStrings>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
}
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListStringsQueryKey(analysisId);
const queryFn: QueryFunction<Awaited<ReturnType<typeof listStrings>>> = ({ signal }) =>
listStrings(analysisId, requestOptions, signal);
return {
queryKey,
queryFn,
enabled: analysisId !== null && analysisId !== undefined,
...queryOptions,
} as UseQueryOptions<Awaited<ReturnType<typeof listStrings>>, TError, TData> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
};
export type ListStringsQueryResult = NonNullable<Awaited<ReturnType<typeof listStrings>>>;
export type ListStringsQueryError = unknown;
export function useListStrings<TData = Awaited<ReturnType<typeof listStrings>>, TError = unknown>(
analysisId: string,
options: {
query: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStrings>>, TError, TData>> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof listStrings>>,
TError,
Awaited<ReturnType<typeof listStrings>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListStrings<TData = Awaited<ReturnType<typeof listStrings>>, TError = unknown>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStrings>>, TError, TData>> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof listStrings>>,
TError,
Awaited<ReturnType<typeof listStrings>>
>,
"initialData"
>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
export function useListStrings<TData = Awaited<ReturnType<typeof listStrings>>, TError = unknown>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStrings>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
/**
* @summary List strings in an analysis
*/
export function useListStrings<TData = Awaited<ReturnType<typeof listStrings>>, TError = unknown>(
analysisId: string,
options?: {
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listStrings>>, TError, TData>>;
request?: SecondParameter<typeof customInstance>;
},
queryClient?: QueryClient
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
const queryOptions = getListStringsQueryOptions(analysisId, options);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* Retrieve memory segments (code, data, etc.) identified during a static analysis.
* @summary List segments in an analysis

View File

@ -4,12 +4,6 @@
* OpenAPI definition
* OpenAPI spec version: v0
*/
export interface EngineResponse {
name: string;
label: string;
available: boolean;
}
export interface WorkspaceRequest {
/** @minLength 1 */
name: string;
@ -141,6 +135,12 @@ export interface XrefResponse {
address?: string;
}
export interface EngineResponse {
name?: string;
label?: string;
available?: boolean;
}
export interface MessageResponse {
id?: string;
sessionId?: string;
@ -162,6 +162,14 @@ export interface AnalysisResponse {
updatedAt?: string;
}
export interface StringResponse {
id?: string;
address?: string;
value?: string;
encoding?: string;
referencedBy?: string;
}
export interface SegmentResponse {
id?: string;
name?: string;
@ -194,11 +202,11 @@ export interface SortObject {
export interface PageableObject {
offset?: number;
paged?: boolean;
pageNumber?: number;
pageSize?: number;
sort?: SortObject;
paged?: boolean;
unpaged?: boolean;
sort?: SortObject;
}
export interface PageFunctionSummaryResponse {
@ -208,10 +216,10 @@ export interface PageFunctionSummaryResponse {
content?: FunctionSummaryResponse[];
number?: number;
pageable?: PageableObject;
numberOfElements?: number;
sort?: SortObject;
first?: boolean;
last?: boolean;
numberOfElements?: number;
empty?: boolean;
}

View File

@ -36,7 +36,7 @@ export const ConfirmDialog = ({
<DialogHeader>
<div className="flex items-center gap-3">
{variant === "destructive" && (
<div className="bg-destructive/10 flex h-10 w-10 items-center justify-center rounded-full">
<div className="bg-destructive/10 flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full">
<AlertTriangle className="text-destructive h-5 w-5" />
</div>
)}

View File

@ -43,7 +43,7 @@ export const AnalyzeDialog = ({
<DialogHeader>
{isReanalyze && (
<div className="flex items-center gap-3">
<div className="bg-destructive/10 flex h-10 w-10 items-center justify-center rounded-full">
<div className="bg-destructive/10 flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full">
<AlertTriangle className="text-destructive h-5 w-5" />
</div>
<div>

View File

@ -0,0 +1,110 @@
import { useState } from "react";
import { Search } from "lucide-react";
import type { StringResponse } from "@/api/generated/api.schemas";
import { cn } from "@/lib/utils";
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 StringTableProps {
strings: StringResponse[];
functionNameToId: Record<string, string>;
onSelectFunction: (functionId: string) => void;
highlightedAddress?: string;
}
export function StringTable({
strings,
functionNameToId,
onSelectFunction,
highlightedAddress,
}: StringTableProps) {
const [search, setSearch] = useState("");
const filtered = strings.filter((s) => s.value?.toLowerCase().includes(search.toLowerCase()));
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={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Filter strings..."
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-[110px]">Address</TableHead>
<TableHead>Value</TableHead>
<TableHead className="w-[90px]">Encoding</TableHead>
<TableHead className="w-[140px]">Referenced by</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((s) => (
<TableRow
key={s.id}
className={cn(
highlightedAddress && s.address === highlightedAddress && "bg-primary/10"
)}
>
<TableCell className="font-mono text-xs">{s.address}</TableCell>
<TableCell className="max-w-[200px] truncate font-mono text-xs">
{s.value}
</TableCell>
<TableCell className="text-muted-foreground text-xs">{s.encoding}</TableCell>
<TableCell className="text-xs">
{s.referencedBy
?.split(/\s+/)
.filter(Boolean)
.map((name, i) => {
const funcId = functionNameToId[name];
return (
<span key={name}>
{i > 0 && " "}
{funcId ? (
<button
type="button"
onClick={() => onSelectFunction(funcId)}
className="text-primary hover:underline"
>
{name}
</button>
) : (
name
)}
</span>
);
})}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{filtered.length === 0 && (
<div className="text-muted-foreground py-4 text-center text-xs">
{search ? "No matching strings" : "No strings found"}
</div>
)}
</ScrollArea>
<div className="border-border text-muted-foreground border-t p-3 text-xs">
{search ? `${filtered.length} of ${strings.length} strings` : `${strings.length} strings`}
</div>
</div>
);
}

View File

@ -1,14 +1,24 @@
"use client";
import { ArrowUpRight, ArrowDownRight, Link, Loader2, GitFork, GripHorizontal } from "lucide-react";
import {
ArrowUpRight,
ArrowDownRight,
Link,
Loader2,
GitFork,
GripHorizontal,
Text,
} from "lucide-react";
import { useGetCallers, useGetCallees } from "@/api/generated/function/function.ts";
import type { XrefResponse } from "@/api/generated/api.schemas";
import type { XrefResponse, StringResponse } from "@/api/generated/api.schemas";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
interface XrefPanelProps {
functionId?: string;
onSelectFunction: (functionId: string) => void;
strings?: StringResponse[];
onSelectString?: (address: string) => void;
}
function XrefTypeBadge({ type }: { type?: string }) {
@ -108,7 +118,12 @@ function XrefList({
);
}
export function XrefPanel({ functionId, onSelectFunction }: XrefPanelProps) {
export function XrefPanel({
functionId,
onSelectFunction,
strings,
onSelectString,
}: XrefPanelProps) {
const { data: callers, isLoading: callersLoading } = useGetCallers(functionId ?? "", {
query: { enabled: !!functionId },
});
@ -146,6 +161,41 @@ export function XrefPanel({ functionId, onSelectFunction }: XrefPanelProps) {
onSelectFunction={onSelectFunction}
isLoading={calleesLoading}
/>
{strings && (
<>
<div className="border-border mx-3 border-t" />
<div className="px-3 py-2">
<h4 className="text-muted-foreground mb-1.5 flex items-center gap-1.5 text-xs font-medium uppercase">
<Text className="h-3 w-3" />
Strings
<span className="text-muted-foreground/60 ml-auto">{strings.length}</span>
</h4>
{strings.length === 0 ? (
<p className="text-muted-foreground/50 py-2 text-center text-xs italic">None</p>
) : (
<div className="space-y-0.5">
{strings.map((s, i) => (
<button
key={s.id ?? i}
type="button"
onClick={() => s.address && onSelectString?.(s.address)}
disabled={!s.address || !onSelectString}
className="hover:bg-muted flex w-full items-center gap-2 rounded-md px-2 py-1 text-left text-xs transition-colors disabled:opacity-50"
>
<span className="text-foreground flex-1 truncate font-mono">{s.value}</span>
{s.address && (
<span className="text-muted-foreground shrink-0 font-mono text-[10px]">
{s.address}
</span>
)}
</button>
))}
</div>
)}
</div>
</>
)}
</ScrollArea>
)}
</div>

View File

@ -4,7 +4,11 @@ import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { useGetBinary, useAnalyzeBinary } from "@/api/generated/binary/binary.ts";
import { useGetProject } from "@/api/generated/project/project.ts";
import { useGetWorkspace } from "@/api/generated/workspace/workspace.ts";
import { useListAnalyses, useListSegments } from "@/api/generated/analysis/analysis.ts";
import {
useListAnalyses,
useListSegments,
useListStrings,
} from "@/api/generated/analysis/analysis.ts";
import { useGetFunction } from "@/api/generated/function/function.ts";
import { useListEngines } from "@/api/generated/engine/engine.ts";
import { customInstance } from "@/api/api.ts";
@ -76,6 +80,11 @@ export function useBinaryPage() {
query: { enabled: !!analysisId },
});
// Fetch strings for the analysis
const { data: strings } = useListStrings(analysisId ?? "", {
query: { enabled: !!analysisId },
});
// Infinite query for functions pagination — single page with all functions
const functionsQuery = useInfiniteQuery({
queryKey: ["functions", analysisId],
@ -138,6 +147,25 @@ export function useBinaryPage() {
return map;
}, [allFunctions, segmentsList]);
// Build map: function name → id (for resolving referencedBy in strings)
const functionNameToId = useMemo(() => {
const map: Record<string, string> = {};
for (const f of allFunctions) {
if (f.name && f.id) map[f.name] = f.id;
}
return map;
}, [allFunctions]);
// Strings referenced by the currently selected function
const stringsList = useMemo(() => strings ?? [], [strings]);
const functionStrings = useMemo(() => {
if (!selectedFunction?.name) return [];
return stringsList.filter((s) => {
if (!s.referencedBy) return false;
return s.referencedBy.split(/\s+/).includes(selectedFunction.name!);
});
}, [stringsList, selectedFunction]);
// Filter functions by segment, flags, and text search (text search is in FunctionTree)
const filteredFunctions = useMemo(() => {
let result = allFunctions;
@ -214,6 +242,9 @@ export function useBinaryPage() {
isFetchingNextPage: functionsQuery.isFetchingNextPage,
functionsLoading: functionsQuery.isLoading,
functionsTotal: functionsQuery.data?.pages[0]?.totalElements ?? 0,
strings: stringsList,
functionNameToId,
functionStrings,
selectedFunction,
selectedFunctionLoading,
analyzeMutation,

View File

@ -1,9 +1,11 @@
import { useState } from "react";
import { ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/EmptyState";
import { Breadcrumbs } from "@/components/Breadcrumbs.tsx";
import { BinaryHeader } from "@/features/binary/BinaryHeader.tsx";
import { FunctionTree } from "@/features/binary/FunctionTree";
import { StringTable } from "@/features/binary/StringTable";
import { XrefPanel } from "@/features/binary/XrefPanel";
import {
ResizableHandle,
@ -14,6 +16,7 @@ import { useBinaryPage } from "@/features/binary/hooks/useBinaryPage";
import { CodeViewer } from "@/features/binary/CodeViewer";
import { AIChatPanel } from "@/features/binary/AIChatPanel";
import { AnalyzeDialog } from "@/features/binary/AnalyzeDialog";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
const Binary = () => {
const {
@ -36,6 +39,9 @@ const Binary = () => {
hasNextPage,
isFetchingNextPage,
functionsTotal,
strings,
functionNameToId,
functionStrings,
selectedFunction,
selectedFunctionLoading,
analyzeMutation,
@ -53,6 +59,19 @@ const Binary = () => {
if (match) selectFunctionByAddress(match[1]);
};
const [activeTab, setActiveTab] = useState("functions");
const [highlightedStringAddress, setHighlightedStringAddress] = useState<string>();
const handleSelectStringInXref = (address: string) => {
setActiveTab("strings");
setHighlightedStringAddress(address);
};
const handleTabChange = (value: string) => {
setActiveTab(value);
setHighlightedStringAddress(undefined);
};
if (binaryLoading) return null;
if (!binary) {
@ -104,24 +123,47 @@ const Binary = () => {
<ResizablePanelGroup orientation="vertical" className="h-full min-h-0 flex-col">
<ResizablePanel id="function-tree-panel" defaultSize={100} minSize={30}>
<div className="flex h-full flex-col overflow-hidden">
<div className="border-border flex shrink-0 items-center border-b px-4 py-2">
<span className="text-sm font-medium">Functions</span>
</div>
<FunctionTree
functions={functions}
selectedFunctionId={selectedFunctionId}
onSelectFunction={(f) => setSelectedFunctionId(f.id)}
onLoadMore={fetchNextPage}
hasMore={!!hasNextPage}
isLoading={isFetchingNextPage}
totalCount={functionsTotal}
segments={segments}
selectedSegmentId={selectedSegmentId}
onSelectSegment={setSelectedSegmentId}
segmentByAddress={segmentByAddress}
selectedFlags={selectedFlags}
onToggleFlag={toggleFlag}
/>
<Tabs
value={activeTab}
onValueChange={handleTabChange}
className="flex min-h-0 flex-1 flex-col"
>
<TabsList className="mx-3 mt-2 shrink-0">
<TabsTrigger value="functions">Functions</TabsTrigger>
<TabsTrigger value="strings">Strings</TabsTrigger>
</TabsList>
<TabsContent
value="functions"
className="mt-1 flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
>
<FunctionTree
functions={functions}
selectedFunctionId={selectedFunctionId}
onSelectFunction={(f) => setSelectedFunctionId(f.id)}
onLoadMore={fetchNextPage}
hasMore={!!hasNextPage}
isLoading={isFetchingNextPage}
totalCount={functionsTotal}
segments={segments}
selectedSegmentId={selectedSegmentId}
onSelectSegment={setSelectedSegmentId}
segmentByAddress={segmentByAddress}
selectedFlags={selectedFlags}
onToggleFlag={toggleFlag}
/>
</TabsContent>
<TabsContent
value="strings"
className="mt-1 flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
>
<StringTable
strings={strings ?? []}
functionNameToId={functionNameToId}
onSelectFunction={setSelectedFunctionId}
highlightedAddress={highlightedStringAddress}
/>
</TabsContent>
</Tabs>
</div>
</ResizablePanel>
@ -141,6 +183,8 @@ const Binary = () => {
<XrefPanel
functionId={selectedFunctionId}
onSelectFunction={setSelectedFunctionId}
strings={functionStrings}
onSelectString={handleSelectStringInXref}
/>
</div>
</ResizablePanel>