feat(analysis): add stack frame, comments, and imports
This commit is contained in:
parent
908d57c3c4
commit
a2c3c1ced9
@ -19,6 +19,7 @@ import type {
|
|||||||
|
|
||||||
import type {
|
import type {
|
||||||
AnalysisResponse,
|
AnalysisResponse,
|
||||||
|
ImportResponse,
|
||||||
ListFunctionsParams,
|
ListFunctionsParams,
|
||||||
PageFunctionSummaryResponse,
|
PageFunctionSummaryResponse,
|
||||||
SegmentResponse,
|
SegmentResponse,
|
||||||
@ -469,6 +470,116 @@ export function useListSegments<TData = Awaited<ReturnType<typeof listSegments>>
|
|||||||
return { ...query, queryKey: queryOptions.queryKey };
|
return { ...query, queryKey: queryOptions.queryKey };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve all imported DLL functions identified during a static analysis.
|
||||||
|
* @summary List imports in an analysis
|
||||||
|
*/
|
||||||
|
export const listImports = (
|
||||||
|
analysisId: string,
|
||||||
|
options?: SecondParameter<typeof customInstance>,
|
||||||
|
signal?: AbortSignal
|
||||||
|
) => {
|
||||||
|
return customInstance<ImportResponse[]>(
|
||||||
|
{ url: `/analysis/${encodeURIComponent(String(analysisId))}/imports`, method: "GET", signal },
|
||||||
|
options
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getListImportsQueryKey = (analysisId: string) => {
|
||||||
|
return [`/analysis/${analysisId}/imports`] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getListImportsQueryOptions = <
|
||||||
|
TData = Awaited<ReturnType<typeof listImports>>,
|
||||||
|
TError = unknown,
|
||||||
|
>(
|
||||||
|
analysisId: string,
|
||||||
|
options?: {
|
||||||
|
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listImports>>, TError, TData>>;
|
||||||
|
request?: SecondParameter<typeof customInstance>;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const { query: queryOptions, request: requestOptions } = options ?? {};
|
||||||
|
|
||||||
|
const queryKey = queryOptions?.queryKey ?? getListImportsQueryKey(analysisId);
|
||||||
|
|
||||||
|
const queryFn: QueryFunction<Awaited<ReturnType<typeof listImports>>> = ({ signal }) =>
|
||||||
|
listImports(analysisId, requestOptions, signal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
queryKey,
|
||||||
|
queryFn,
|
||||||
|
enabled: analysisId !== null && analysisId !== undefined,
|
||||||
|
...queryOptions,
|
||||||
|
} as UseQueryOptions<Awaited<ReturnType<typeof listImports>>, TError, TData> & {
|
||||||
|
queryKey: DataTag<QueryKey, TData, TError>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListImportsQueryResult = NonNullable<Awaited<ReturnType<typeof listImports>>>;
|
||||||
|
export type ListImportsQueryError = unknown;
|
||||||
|
|
||||||
|
export function useListImports<TData = Awaited<ReturnType<typeof listImports>>, TError = unknown>(
|
||||||
|
analysisId: string,
|
||||||
|
options: {
|
||||||
|
query: Partial<UseQueryOptions<Awaited<ReturnType<typeof listImports>>, TError, TData>> &
|
||||||
|
Pick<
|
||||||
|
DefinedInitialDataOptions<
|
||||||
|
Awaited<ReturnType<typeof listImports>>,
|
||||||
|
TError,
|
||||||
|
Awaited<ReturnType<typeof listImports>>
|
||||||
|
>,
|
||||||
|
"initialData"
|
||||||
|
>;
|
||||||
|
request?: SecondParameter<typeof customInstance>;
|
||||||
|
},
|
||||||
|
queryClient?: QueryClient
|
||||||
|
): DefinedUseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||||
|
export function useListImports<TData = Awaited<ReturnType<typeof listImports>>, TError = unknown>(
|
||||||
|
analysisId: string,
|
||||||
|
options?: {
|
||||||
|
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listImports>>, TError, TData>> &
|
||||||
|
Pick<
|
||||||
|
UndefinedInitialDataOptions<
|
||||||
|
Awaited<ReturnType<typeof listImports>>,
|
||||||
|
TError,
|
||||||
|
Awaited<ReturnType<typeof listImports>>
|
||||||
|
>,
|
||||||
|
"initialData"
|
||||||
|
>;
|
||||||
|
request?: SecondParameter<typeof customInstance>;
|
||||||
|
},
|
||||||
|
queryClient?: QueryClient
|
||||||
|
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||||
|
export function useListImports<TData = Awaited<ReturnType<typeof listImports>>, TError = unknown>(
|
||||||
|
analysisId: string,
|
||||||
|
options?: {
|
||||||
|
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listImports>>, TError, TData>>;
|
||||||
|
request?: SecondParameter<typeof customInstance>;
|
||||||
|
},
|
||||||
|
queryClient?: QueryClient
|
||||||
|
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> };
|
||||||
|
/**
|
||||||
|
* @summary List imports in an analysis
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function useListImports<TData = Awaited<ReturnType<typeof listImports>>, TError = unknown>(
|
||||||
|
analysisId: string,
|
||||||
|
options?: {
|
||||||
|
query?: Partial<UseQueryOptions<Awaited<ReturnType<typeof listImports>>, TError, TData>>;
|
||||||
|
request?: SecondParameter<typeof customInstance>;
|
||||||
|
},
|
||||||
|
queryClient?: QueryClient
|
||||||
|
): UseQueryResult<TData, TError> & { queryKey: DataTag<QueryKey, TData, TError> } {
|
||||||
|
const queryOptions = getListImportsQueryOptions(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.
|
* Retrieve paginated list of functions extracted during a static analysis.
|
||||||
* @summary List functions in an analysis
|
* @summary List functions in an analysis
|
||||||
|
|||||||
@ -104,6 +104,18 @@ export interface JobListResponse {
|
|||||||
failed?: number;
|
failed?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CommentResponse {
|
||||||
|
address?: string;
|
||||||
|
text?: string;
|
||||||
|
repeatable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FrameMemberResponse {
|
||||||
|
offset?: number;
|
||||||
|
size?: number;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LabelResponse {
|
export interface LabelResponse {
|
||||||
name?: string;
|
name?: string;
|
||||||
address?: string;
|
address?: string;
|
||||||
@ -123,6 +135,8 @@ export interface FunctionDetailResponse {
|
|||||||
callerCount?: number;
|
callerCount?: number;
|
||||||
calleeCount?: number;
|
calleeCount?: number;
|
||||||
labels?: LabelResponse[];
|
labels?: LabelResponse[];
|
||||||
|
stackFrame?: FrameMemberResponse[];
|
||||||
|
comments?: CommentResponse[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface XrefResponse {
|
export interface XrefResponse {
|
||||||
@ -177,6 +191,14 @@ export interface SegmentResponse {
|
|||||||
endAddress?: string;
|
endAddress?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImportResponse {
|
||||||
|
id?: string;
|
||||||
|
dll?: string;
|
||||||
|
name?: string;
|
||||||
|
address?: string;
|
||||||
|
ordinal?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Pageable {
|
export interface Pageable {
|
||||||
/** @minimum 0 */
|
/** @minimum 0 */
|
||||||
page?: number;
|
page?: number;
|
||||||
@ -202,11 +224,11 @@ export interface SortObject {
|
|||||||
|
|
||||||
export interface PageableObject {
|
export interface PageableObject {
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
paged?: boolean;
|
||||||
pageNumber?: number;
|
pageNumber?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
paged?: boolean;
|
|
||||||
unpaged?: boolean;
|
|
||||||
sort?: SortObject;
|
sort?: SortObject;
|
||||||
|
unpaged?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageFunctionSummaryResponse {
|
export interface PageFunctionSummaryResponse {
|
||||||
@ -216,10 +238,10 @@ export interface PageFunctionSummaryResponse {
|
|||||||
content?: FunctionSummaryResponse[];
|
content?: FunctionSummaryResponse[];
|
||||||
number?: number;
|
number?: number;
|
||||||
pageable?: PageableObject;
|
pageable?: PageableObject;
|
||||||
numberOfElements?: number;
|
|
||||||
sort?: SortObject;
|
sort?: SortObject;
|
||||||
first?: boolean;
|
first?: boolean;
|
||||||
last?: boolean;
|
last?: boolean;
|
||||||
|
numberOfElements?: number;
|
||||||
empty?: boolean;
|
empty?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRef, useCallback } from "react";
|
import { useRef, useCallback, useMemo } from "react";
|
||||||
import { Copy, Code, ArrowUpRight, ArrowDownRight, Link } from "lucide-react";
|
import { Copy, Code, ArrowUpRight, ArrowDownRight, Link } from "lucide-react";
|
||||||
import type { FunctionDetailResponse } from "@/api/generated/api.schemas";
|
import type { FunctionDetailResponse, CommentResponse } from "@/api/generated/api.schemas";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@ -11,6 +11,8 @@ import { Button } from "@/components/ui/button";
|
|||||||
interface CodeViewerProps {
|
interface CodeViewerProps {
|
||||||
func: FunctionDetailResponse | undefined;
|
func: FunctionDetailResponse | undefined;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
onSelectFunction: (id: string) => void;
|
||||||
|
functionNameToId: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FLAG_COLORS: Record<string, string> = {
|
const FLAG_COLORS: Record<string, string> = {
|
||||||
@ -50,7 +52,12 @@ function LabelTypeIcon({ type }: { type: string | undefined }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CodeViewer({ func, isLoading }: CodeViewerProps) {
|
export function CodeViewer({
|
||||||
|
func,
|
||||||
|
isLoading,
|
||||||
|
onSelectFunction,
|
||||||
|
functionNameToId,
|
||||||
|
}: CodeViewerProps) {
|
||||||
const assemblyRef = useRef<HTMLTableElement>(null);
|
const assemblyRef = useRef<HTMLTableElement>(null);
|
||||||
const pseudoRef = useRef<HTMLTableElement>(null);
|
const pseudoRef = useRef<HTMLTableElement>(null);
|
||||||
|
|
||||||
@ -85,6 +92,22 @@ export function CodeViewer({ func, isLoading }: CodeViewerProps) {
|
|||||||
addressToLabel[func.address] = func.name;
|
addressToLabel[func.address] = func.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build comments by address for inline rendering
|
||||||
|
const commentsByAddress = useMemo(() => {
|
||||||
|
const map: Record<string, CommentResponse[]> = {};
|
||||||
|
for (const c of func?.comments ?? []) {
|
||||||
|
if (!c.address) continue;
|
||||||
|
const key = c.address.toLowerCase();
|
||||||
|
if (!map[key]) map[key] = [];
|
||||||
|
map[key].push(c);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [func?.comments]);
|
||||||
|
|
||||||
|
// Stack frame table
|
||||||
|
const stackFrame = func?.stackFrame ?? [];
|
||||||
|
const hasStackFrame = stackFrame.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs defaultValue="assembly" className="flex h-full flex-col">
|
<Tabs defaultValue="assembly" className="flex h-full flex-col">
|
||||||
<div className="border-border flex shrink-0 items-center justify-between border-b px-4 py-2">
|
<div className="border-border flex shrink-0 items-center justify-between border-b px-4 py-2">
|
||||||
@ -129,6 +152,32 @@ export function CodeViewer({ func, isLoading }: CodeViewerProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{hasStackFrame && (
|
||||||
|
<div className="border-border shrink-0 border-b px-4 py-1">
|
||||||
|
<details>
|
||||||
|
<summary className="text-muted-foreground cursor-pointer text-xs select-none">
|
||||||
|
Stack Frame ({stackFrame.length} member{stackFrame.length !== 1 ? "s" : ""})
|
||||||
|
</summary>
|
||||||
|
<div className="mt-1 flex flex-col">
|
||||||
|
<div className="text-muted-foreground flex shrink-0 text-xs">
|
||||||
|
<span className="w-[60px]">Offset</span>
|
||||||
|
<span className="w-[50px]">Size</span>
|
||||||
|
<span>Name</span>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-40 overflow-y-auto">
|
||||||
|
{stackFrame.map((m, i) => (
|
||||||
|
<div key={i} className="even:bg-muted/30 flex font-mono text-xs">
|
||||||
|
<span className="w-[60px] shrink-0">{m.offset}</span>
|
||||||
|
<span className="w-[50px] shrink-0">{m.size}</span>
|
||||||
|
<span className="truncate">{m.name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<TabsContent value="assembly" className="mt-0 flex-1 overflow-hidden">
|
<TabsContent value="assembly" className="mt-0 flex-1 overflow-hidden">
|
||||||
{hasData ? (
|
{hasData ? (
|
||||||
<ScrollArea className="h-full w-full">
|
<ScrollArea className="h-full w-full">
|
||||||
@ -143,6 +192,9 @@ export function CodeViewer({ func, isLoading }: CodeViewerProps) {
|
|||||||
addressToLabel={addressToLabel}
|
addressToLabel={addressToLabel}
|
||||||
labelToAddress={labelToAddress}
|
labelToAddress={labelToAddress}
|
||||||
addressToLabelType={addressToLabelType}
|
addressToLabelType={addressToLabelType}
|
||||||
|
commentsByAddress={commentsByAddress}
|
||||||
|
onSelectFunction={onSelectFunction}
|
||||||
|
functionNameToId={functionNameToId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -214,12 +266,18 @@ function AssemblyRow({
|
|||||||
addressToLabel,
|
addressToLabel,
|
||||||
labelToAddress,
|
labelToAddress,
|
||||||
addressToLabelType,
|
addressToLabelType,
|
||||||
|
commentsByAddress,
|
||||||
|
onSelectFunction,
|
||||||
|
functionNameToId,
|
||||||
}: {
|
}: {
|
||||||
line: string;
|
line: string;
|
||||||
lineNumber: number;
|
lineNumber: number;
|
||||||
addressToLabel: Record<string, string>;
|
addressToLabel: Record<string, string>;
|
||||||
labelToAddress: Record<string, string>;
|
labelToAddress: Record<string, string>;
|
||||||
addressToLabelType: Record<string, string>;
|
addressToLabelType: Record<string, string>;
|
||||||
|
commentsByAddress: Record<string, CommentResponse[]>;
|
||||||
|
onSelectFunction: (id: string) => void;
|
||||||
|
functionNameToId: Record<string, string>;
|
||||||
}) {
|
}) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
|
|
||||||
@ -229,7 +287,7 @@ function AssemblyRow({
|
|||||||
<td className="w-12 pr-4 text-right text-[var(--code-line-number)] select-none">
|
<td className="w-12 pr-4 text-right text-[var(--code-line-number)] select-none">
|
||||||
{lineNumber}
|
{lineNumber}
|
||||||
</td>
|
</td>
|
||||||
<td colSpan={3}> </td>
|
<td colSpan={4}> </td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -250,7 +308,7 @@ function AssemblyRow({
|
|||||||
<td className="w-[14ch] whitespace-nowrap">
|
<td className="w-[14ch] whitespace-nowrap">
|
||||||
<span className="text-[var(--code-function)]">{labelName}:</span>
|
<span className="text-[var(--code-function)]">{labelName}:</span>
|
||||||
</td>
|
</td>
|
||||||
<td colSpan={2}> </td>
|
<td colSpan={3}> </td>
|
||||||
</tr>
|
</tr>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
@ -260,6 +318,22 @@ function AssemblyRow({
|
|||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Inject backend comments anchored to this address — rendered in a dedicated column
|
||||||
|
const backendComments = address ? commentsByAddress[address.toLowerCase()] : undefined;
|
||||||
|
const commentsCell =
|
||||||
|
backendComments && backendComments.length > 0 ? (
|
||||||
|
<td className="text-xs text-[var(--code-comment)]">
|
||||||
|
{backendComments.map((c, ci) => {
|
||||||
|
const body = (c.text ?? "").replace(/^;\s*/, "");
|
||||||
|
return (
|
||||||
|
<span key={`ida-cmt-${ci}`}>
|
||||||
|
{ci > 0 && " "}; {body}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</td>
|
||||||
|
) : null;
|
||||||
|
|
||||||
// Label-only line (ends with colon, no mnemonic)
|
// Label-only line (ends with colon, no mnemonic)
|
||||||
const labelMatch = rest.match(/^([a-zA-Z_]\w*):$/);
|
const labelMatch = rest.match(/^([a-zA-Z_]\w*):$/);
|
||||||
if (labelMatch) {
|
if (labelMatch) {
|
||||||
@ -271,9 +345,8 @@ function AssemblyRow({
|
|||||||
{lineNumber}
|
{lineNumber}
|
||||||
</td>
|
</td>
|
||||||
{addrCell}
|
{addrCell}
|
||||||
<td className="text-[var(--code-function)]" colSpan={2}>
|
<td className="text-[var(--code-function)]">{rest}</td>
|
||||||
{rest}
|
{commentsCell}
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -289,9 +362,8 @@ function AssemblyRow({
|
|||||||
{lineNumber}
|
{lineNumber}
|
||||||
</td>
|
</td>
|
||||||
{addrCell}
|
{addrCell}
|
||||||
<td className="text-[var(--code-comment)]" colSpan={2}>
|
<td className="text-[var(--code-comment)]">{rest}</td>
|
||||||
{rest}
|
{commentsCell}
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -319,8 +391,11 @@ function AssemblyRow({
|
|||||||
operands={operands}
|
operands={operands}
|
||||||
labelToAddress={labelToAddress}
|
labelToAddress={labelToAddress}
|
||||||
labelTypeMap={addressToLabelType}
|
labelTypeMap={addressToLabelType}
|
||||||
|
onSelectFunction={onSelectFunction}
|
||||||
|
functionNameToId={functionNameToId}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
{commentsCell}
|
||||||
</tr>
|
</tr>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -335,9 +410,8 @@ function AssemblyRow({
|
|||||||
{lineNumber}
|
{lineNumber}
|
||||||
</td>
|
</td>
|
||||||
{addrCell}
|
{addrCell}
|
||||||
<td className="text-foreground" colSpan={2}>
|
<td className="text-foreground">{rest}</td>
|
||||||
{rest}
|
{commentsCell}
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -347,10 +421,14 @@ function HighlightedAssembly({
|
|||||||
operands,
|
operands,
|
||||||
labelToAddress,
|
labelToAddress,
|
||||||
labelTypeMap,
|
labelTypeMap,
|
||||||
|
onSelectFunction,
|
||||||
|
functionNameToId,
|
||||||
}: {
|
}: {
|
||||||
operands: string;
|
operands: string;
|
||||||
labelToAddress: Record<string, string>;
|
labelToAddress: Record<string, string>;
|
||||||
labelTypeMap: Record<string, string>;
|
labelTypeMap: Record<string, string>;
|
||||||
|
onSelectFunction: (id: string) => void;
|
||||||
|
functionNameToId: Record<string, string>;
|
||||||
}) {
|
}) {
|
||||||
const jumpToLabel = (name: string) => {
|
const jumpToLabel = (name: string) => {
|
||||||
document.getElementById(`asm-label-${name}`)?.scrollIntoView({
|
document.getElementById(`asm-label-${name}`)?.scrollIntoView({
|
||||||
@ -359,9 +437,28 @@ function HighlightedAssembly({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const parts = operands.split(
|
const handleLabelClick = (label: string) => {
|
||||||
/(\b0x[0-9a-fA-F]+\b|\bloc_\w+\b|\bsub_\w+\b|\bshort\b|\bbyte\b|\bword\b|\bdword\b|\bptr\b|\bnear\b|\bfar\b)/g
|
const addr = labelToAddress[label];
|
||||||
);
|
if (addr) {
|
||||||
|
const ltype = labelTypeMap[addr];
|
||||||
|
// CALL / JUMP labels are cross-references to external functions — prefer navigation
|
||||||
|
if (ltype === "CALL" || ltype === "JUMP") {
|
||||||
|
const funcId = functionNameToId[label];
|
||||||
|
if (funcId) {
|
||||||
|
onSelectFunction(funcId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jumpToLabel(label);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const funcId = functionNameToId[label];
|
||||||
|
if (funcId) onSelectFunction(funcId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const KEYWORDS = new Set(["short", "byte", "word", "dword", "ptr", "near", "far"]);
|
||||||
|
|
||||||
|
const parts = operands.split(/(\b0x[0-9a-fA-F]+\b|\b[a-zA-Z_]\w*\b)/g);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
@ -373,9 +470,18 @@ function HighlightedAssembly({
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (/^(loc_\w+|sub_\w+)$/.test(part)) {
|
if (/^[a-zA-Z_]\w*$/.test(part)) {
|
||||||
const addr = labelToAddress[part];
|
if (KEYWORDS.has(part)) {
|
||||||
const ltype = addr ? labelTypeMap[addr] : undefined;
|
return (
|
||||||
|
<span key={i} className="text-[var(--code-keyword)]">
|
||||||
|
{part}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const isLocal = labelToAddress[part];
|
||||||
|
const isFunc = functionNameToId[part];
|
||||||
|
if (isLocal || isFunc) {
|
||||||
|
const ltype = isLocal ? labelTypeMap[labelToAddress[part]] : undefined;
|
||||||
const lcolor =
|
const lcolor =
|
||||||
ltype === "CALL"
|
ltype === "CALL"
|
||||||
? "text-[var(--code-function)]"
|
? "text-[var(--code-function)]"
|
||||||
@ -386,19 +492,16 @@ function HighlightedAssembly({
|
|||||||
<span
|
<span
|
||||||
key={i}
|
key={i}
|
||||||
className={`cursor-pointer hover:underline ${lcolor}`}
|
className={`cursor-pointer hover:underline ${lcolor}`}
|
||||||
title={addr ? `→ ${addr}${ltype ? ` (${ltype})` : ""}` : undefined}
|
title={
|
||||||
onClick={() => jumpToLabel(part)}
|
isLocal ? `→ ${labelToAddress[part]}${ltype ? ` (${ltype})` : ""}` : `→ ${part}`
|
||||||
|
}
|
||||||
|
onClick={() => handleLabelClick(part)}
|
||||||
>
|
>
|
||||||
{part}
|
{part}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (/^(short|byte|word|dword|ptr|near|far)$/.test(part)) {
|
return <span key={i}>{part}</span>;
|
||||||
return (
|
|
||||||
<span key={i} className="text-[var(--code-keyword)]">
|
|
||||||
{part}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return <span key={i}>{part}</span>;
|
return <span key={i}>{part}</span>;
|
||||||
})}
|
})}
|
||||||
|
|||||||
75
src/features/binary/ImportTable.tsx
Normal file
75
src/features/binary/ImportTable.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Search } from "lucide-react";
|
||||||
|
import type { ImportResponse } 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 ImportTableProps {
|
||||||
|
imports: ImportResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImportTable({ imports }: ImportTableProps) {
|
||||||
|
const [dllFilter, setDllFilter] = useState("");
|
||||||
|
|
||||||
|
const filtered = imports.filter(
|
||||||
|
(i) => !dllFilter || i.dll?.toLowerCase().includes(dllFilter.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={dllFilter}
|
||||||
|
onChange={(e) => setDllFilter(e.target.value)}
|
||||||
|
placeholder="Filter by DLL..."
|
||||||
|
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-[130px]">DLL</TableHead>
|
||||||
|
<TableHead>Function</TableHead>
|
||||||
|
<TableHead className="w-[110px]">Address</TableHead>
|
||||||
|
<TableHead className="w-[90px] text-right">Ordinal</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filtered.map((imp) => (
|
||||||
|
<TableRow key={imp.id}>
|
||||||
|
<TableCell className="text-xs">{imp.dll}</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs">{imp.name}</TableCell>
|
||||||
|
<TableCell className="font-mono text-xs">{imp.address}</TableCell>
|
||||||
|
<TableCell className="text-right text-xs">{imp.ordinal}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
{filtered.length === 0 && (
|
||||||
|
<div className="text-muted-foreground py-4 text-center text-xs">
|
||||||
|
{dllFilter ? "No matching imports" : "No imports found"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
<div className="border-border text-muted-foreground border-t p-3 text-xs">
|
||||||
|
{dllFilter
|
||||||
|
? `${filtered.length} of ${imports.length} imports`
|
||||||
|
: `${imports.length} imports`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ import {
|
|||||||
useListAnalyses,
|
useListAnalyses,
|
||||||
useListSegments,
|
useListSegments,
|
||||||
useListStrings,
|
useListStrings,
|
||||||
|
useListImports,
|
||||||
} from "@/api/generated/analysis/analysis.ts";
|
} from "@/api/generated/analysis/analysis.ts";
|
||||||
import { useGetFunction } from "@/api/generated/function/function.ts";
|
import { useGetFunction } from "@/api/generated/function/function.ts";
|
||||||
import { useListEngines } from "@/api/generated/engine/engine.ts";
|
import { useListEngines } from "@/api/generated/engine/engine.ts";
|
||||||
@ -85,6 +86,11 @@ export function useBinaryPage() {
|
|||||||
query: { enabled: !!analysisId },
|
query: { enabled: !!analysisId },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fetch imports for the analysis
|
||||||
|
const { data: imports } = useListImports(analysisId ?? "", {
|
||||||
|
query: { enabled: !!analysisId },
|
||||||
|
});
|
||||||
|
|
||||||
// Infinite query for functions pagination — single page with all functions
|
// Infinite query for functions pagination — single page with all functions
|
||||||
const functionsQuery = useInfiniteQuery({
|
const functionsQuery = useInfiniteQuery({
|
||||||
queryKey: ["functions", analysisId],
|
queryKey: ["functions", analysisId],
|
||||||
@ -243,6 +249,7 @@ export function useBinaryPage() {
|
|||||||
functionsLoading: functionsQuery.isLoading,
|
functionsLoading: functionsQuery.isLoading,
|
||||||
functionsTotal: functionsQuery.data?.pages[0]?.totalElements ?? 0,
|
functionsTotal: functionsQuery.data?.pages[0]?.totalElements ?? 0,
|
||||||
strings: stringsList,
|
strings: stringsList,
|
||||||
|
imports: imports ?? [],
|
||||||
functionNameToId,
|
functionNameToId,
|
||||||
functionStrings,
|
functionStrings,
|
||||||
selectedFunction,
|
selectedFunction,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { Breadcrumbs } from "@/components/Breadcrumbs.tsx";
|
|||||||
import { BinaryHeader } from "@/features/binary/BinaryHeader.tsx";
|
import { BinaryHeader } from "@/features/binary/BinaryHeader.tsx";
|
||||||
import { FunctionTree } from "@/features/binary/FunctionTree";
|
import { FunctionTree } from "@/features/binary/FunctionTree";
|
||||||
import { StringTable } from "@/features/binary/StringTable";
|
import { StringTable } from "@/features/binary/StringTable";
|
||||||
|
import { ImportTable } from "@/features/binary/ImportTable";
|
||||||
import { XrefPanel } from "@/features/binary/XrefPanel";
|
import { XrefPanel } from "@/features/binary/XrefPanel";
|
||||||
import {
|
import {
|
||||||
ResizableHandle,
|
ResizableHandle,
|
||||||
@ -51,6 +52,7 @@ const Binary = () => {
|
|||||||
handleAnalyzeConfirm,
|
handleAnalyzeConfirm,
|
||||||
engines,
|
engines,
|
||||||
enginesLoading,
|
enginesLoading,
|
||||||
|
imports,
|
||||||
} = useBinaryPage();
|
} = useBinaryPage();
|
||||||
|
|
||||||
const navigateToEntryPoint = () => {
|
const navigateToEntryPoint = () => {
|
||||||
@ -131,6 +133,9 @@ const Binary = () => {
|
|||||||
<TabsList className="mx-3 mt-2 shrink-0">
|
<TabsList className="mx-3 mt-2 shrink-0">
|
||||||
<TabsTrigger value="functions">Functions</TabsTrigger>
|
<TabsTrigger value="functions">Functions</TabsTrigger>
|
||||||
<TabsTrigger value="strings">Strings</TabsTrigger>
|
<TabsTrigger value="strings">Strings</TabsTrigger>
|
||||||
|
{imports && imports.length > 0 && (
|
||||||
|
<TabsTrigger value="imports">Imports</TabsTrigger>
|
||||||
|
)}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent
|
<TabsContent
|
||||||
value="functions"
|
value="functions"
|
||||||
@ -163,6 +168,14 @@ const Binary = () => {
|
|||||||
highlightedAddress={highlightedStringAddress}
|
highlightedAddress={highlightedStringAddress}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
{imports && imports.length > 0 && (
|
||||||
|
<TabsContent
|
||||||
|
value="imports"
|
||||||
|
className="mt-1 flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
|
||||||
|
>
|
||||||
|
<ImportTable imports={imports} />
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
@ -196,7 +209,12 @@ const Binary = () => {
|
|||||||
|
|
||||||
<ResizablePanel id="code-panel" defaultSize="40%" minSize="30%">
|
<ResizablePanel id="code-panel" defaultSize="40%" minSize="30%">
|
||||||
<div className="relative flex h-full flex-col overflow-hidden bg-(--code-bg)">
|
<div className="relative flex h-full flex-col overflow-hidden bg-(--code-bg)">
|
||||||
<CodeViewer func={selectedFunction} isLoading={selectedFunctionLoading} />
|
<CodeViewer
|
||||||
|
func={selectedFunction}
|
||||||
|
isLoading={selectedFunctionLoading}
|
||||||
|
onSelectFunction={setSelectedFunctionId}
|
||||||
|
functionNameToId={functionNameToId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user