diff --git a/src/api/generated/api.schemas.ts b/src/api/generated/api.schemas.ts index 205038c..55e7255 100644 --- a/src/api/generated/api.schemas.ts +++ b/src/api/generated/api.schemas.ts @@ -4,6 +4,12 @@ * OpenAPI definition * OpenAPI spec version: v0 */ +export interface EngineResponse { + name: string; + label: string; + available: boolean; +} + export interface WorkspaceRequest { /** @minLength 1 */ name: string; diff --git a/src/api/generated/engine/engine.ts b/src/api/generated/engine/engine.ts new file mode 100644 index 0000000..6b76587 --- /dev/null +++ b/src/api/generated/engine/engine.ts @@ -0,0 +1,120 @@ +/** + * Generated by orval v8.15.0 🍺 + * Do not edit manually. + * OpenAPI definition + * OpenAPI spec version: v0 + */ +import { useQuery } from "@tanstack/react-query"; +import type { + DataTag, + DefinedInitialDataOptions, + DefinedUseQueryResult, + QueryClient, + QueryFunction, + QueryKey, + UndefinedInitialDataOptions, + UseQueryOptions, + UseQueryResult, +} from "@tanstack/react-query"; + +import type { EngineResponse } from "../api.schemas"; + +import { customInstance } from "../../api"; + +type SecondParameter unknown> = Parameters[1]; + +/** + * Retrieve all registered static analysis engines with their availability status. + * @summary List available analysis engines + */ +export const listEngines = ( + options?: SecondParameter, + signal?: AbortSignal +) => { + return customInstance({ url: `/engines`, method: "GET", signal }, options); +}; + +export const getListEnginesQueryKey = () => { + return [`/engines`] as const; +}; + +export const getListEnginesQueryOptions = < + TData = Awaited>, + TError = unknown, +>(options?: { + query?: Partial>, TError, TData>>; + request?: SecondParameter; +}) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = queryOptions?.queryKey ?? getListEnginesQueryKey(); + + const queryFn: QueryFunction>> = ({ signal }) => + listEngines(requestOptions, signal); + + return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type ListEnginesQueryResult = NonNullable>>; +export type ListEnginesQueryError = unknown; + +export function useListEngines>, TError = unknown>( + options: { + query: Partial>, TError, TData>> & + Pick< + DefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): DefinedUseQueryResult & { queryKey: DataTag }; +export function useListEngines>, TError = unknown>( + options?: { + query?: Partial>, TError, TData>> & + Pick< + UndefinedInitialDataOptions< + Awaited>, + TError, + Awaited> + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +export function useListEngines>, TError = unknown>( + options?: { + query?: Partial>, TError, TData>>; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag }; +/** + * @summary List available analysis engines + */ + +export function useListEngines>, TError = unknown>( + options?: { + query?: Partial>, TError, TData>>; + request?: SecondParameter; + }, + queryClient?: QueryClient +): UseQueryResult & { queryKey: DataTag } { + const queryOptions = getListEnginesQueryOptions(options); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult & { + queryKey: DataTag; + }; + + return { ...query, queryKey: queryOptions.queryKey }; +} diff --git a/src/components/EngineSelect.tsx b/src/components/EngineSelect.tsx new file mode 100644 index 0000000..253a8ab --- /dev/null +++ b/src/components/EngineSelect.tsx @@ -0,0 +1,57 @@ +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Loader2 } from "lucide-react"; +import type { EngineResponse } from "@/api/generated/api.schemas"; +import { useMemo } from "react"; + +interface EngineSelectProps { + engines: EngineResponse[]; + value?: string; + onChange: (engine: string) => void; + placeholder?: string; + isLoading?: boolean; + disabled?: boolean; +} + +export const EngineSelect = ({ + engines, + value, + onChange, + placeholder = "Select engine", + isLoading = false, + disabled = false, +}: EngineSelectProps) => { + const available = useMemo(() => engines.filter((e) => e.available), [engines]); + + return ( + + ); +}; diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index e41f2b1..20513ff 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -109,7 +109,7 @@ const SelectItem = React.forwardRef< void; + isReanalyze: boolean; + engines: EngineResponse[]; + enginesLoading: boolean; + onConfirm: (engine: string) => void; +} + +export const AnalyzeDialog = ({ + open, + onOpenChange, + isReanalyze, + engines, + enginesLoading, + onConfirm, +}: AnalyzeDialogProps) => { + const [selectedEngine, setSelectedEngine] = useState(); + + const handleConfirm = () => { + if (!selectedEngine) return; + onConfirm(selectedEngine); + onOpenChange(false); + }; + + return ( + + + + {isReanalyze && ( +
+
+ +
+
+ Re-analyze binary + + This will replace all current analysis data. Function names, comments, and + decompiled code will be lost. + +
+
+ )} + {!isReanalyze && ( +
+ Analyze binary + + Select the analysis engine to use. + +
+ )} +
+ +
+
+ + +
+
+ + + + + +
+
+ ); +}; diff --git a/src/features/binary/hooks/useBinaryPage.ts b/src/features/binary/hooks/useBinaryPage.ts index 434fee8..23bdfb6 100644 --- a/src/features/binary/hooks/useBinaryPage.ts +++ b/src/features/binary/hooks/useBinaryPage.ts @@ -6,6 +6,7 @@ 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 { useGetFunction } from "@/api/generated/function/function.ts"; +import { useListEngines } from "@/api/generated/engine/engine.ts"; import { customInstance } from "@/api/api.ts"; import type { FunctionSummaryResponse, @@ -42,7 +43,7 @@ export function useBinaryPage() { const queryClient = useQueryClient(); const [selectedFunctionId, setSelectedFunctionId] = useState(); - const [confirmReanalyzeOpen, setConfirmReanalyzeOpen] = useState(false); + const [analyzeDialogOpen, setAnalyzeDialogOpen] = useState(false); const [selectedSegmentId, setSelectedSegmentId] = useState(); const [selectedFlags, setSelectedFlags] = useState>(new Set()); @@ -96,6 +97,9 @@ export function useBinaryPage() { { query: { enabled: !!selectedFunctionId } } ); + // Fetch available engines + const { data: engines, isLoading: enginesLoading } = useListEngines(); + // Analyze / re-analyze mutation const analyzeMutation = useAnalyzeBinary({ mutation: { @@ -107,17 +111,13 @@ export function useBinaryPage() { }, }); - const handleAnalyze = () => { - analyzeMutation.mutate({ binaryId, params: { engine: "IDA5" } }); + const handleOpenAnalyzeDialog = () => { + setAnalyzeDialogOpen(true); }; - const handleReanalyzeRequest = () => { - setConfirmReanalyzeOpen(true); - }; - - const handleReanalyzeConfirm = () => { - setConfirmReanalyzeOpen(false); - handleAnalyze(); + const handleAnalyzeConfirm = (engine: string) => { + setAnalyzeDialogOpen(false); + analyzeMutation.mutate({ binaryId, params: { engine } }); }; // Flatten all pages into a single array @@ -217,10 +217,11 @@ export function useBinaryPage() { selectedFunction, selectedFunctionLoading, analyzeMutation, - confirmReanalyzeOpen, - setConfirmReanalyzeOpen, - handleAnalyze, - handleReanalyzeRequest, - handleReanalyzeConfirm, + analyzeDialogOpen, + setAnalyzeDialogOpen, + handleOpenAnalyzeDialog, + handleAnalyzeConfirm, + engines: engines ?? [], + enginesLoading, }; } diff --git a/src/features/project/FileUpload.tsx b/src/features/project/FileUpload.tsx index db3ae28..a9e670d 100644 --- a/src/features/project/FileUpload.tsx +++ b/src/features/project/FileUpload.tsx @@ -13,6 +13,8 @@ interface FileUploadProps { accept?: string; maxSize?: number; className?: string; + autoUpload?: boolean; + onFileSelect?: (file: File) => void; } export const FileUpload = ({ @@ -23,6 +25,8 @@ export const FileUpload = ({ accept = ".exe,.com,.dll,.bin", maxSize = 50 * 1024 * 1024, className, + autoUpload = true, + onFileSelect, }: FileUploadProps) => { const [isDragging, setIsDragging] = useState(false); const [file, setFile] = useState(null); @@ -49,9 +53,13 @@ export const FileUpload = ({ setValidationError(null); setFile(file); - onUpload(file); + if (autoUpload) { + onUpload(file); + } else { + onFileSelect?.(file); + } }, - [onUpload, maxSize] + [onUpload, maxSize, autoUpload, onFileSelect] ); const handleDrop = useCallback( diff --git a/src/features/project/UploadBinaryDialog.tsx b/src/features/project/UploadBinaryDialog.tsx index b08ae4b..435ba07 100644 --- a/src/features/project/UploadBinaryDialog.tsx +++ b/src/features/project/UploadBinaryDialog.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { Dialog, DialogContent, @@ -8,14 +9,19 @@ import { } from "@/components/ui/dialog.tsx"; import { Button } from "@/components/ui/button.tsx"; import { FileUpload } from "@/features/project/FileUpload.tsx"; +import { EngineSelect } from "@/components/EngineSelect"; +import { Upload } from "lucide-react"; +import type { EngineResponse } from "@/api/generated/api.schemas"; interface UploadBinaryDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - onUpload: (file: File) => void; + onUpload: (file: File, engine?: string) => void; isUploading: boolean; uploadError: string | null; onReset: () => void; + engines: EngineResponse[]; + enginesLoading: boolean; } export const UploadBinaryDialog = ({ @@ -25,7 +31,31 @@ export const UploadBinaryDialog = ({ isUploading, uploadError, onReset, + engines, + enginesLoading, }: UploadBinaryDialogProps) => { + const [selectedFile, setSelectedFile] = useState(null); + const [selectedEngine, setSelectedEngine] = useState(); + + const handleFileSelect = (file: File) => { + setSelectedFile(file); + }; + + const handleEngineChange = (engine: string) => { + setSelectedEngine(engine || undefined); + }; + + const handleUpload = () => { + if (!selectedFile) return; + onUpload(selectedFile, selectedEngine); + }; + + const handleReset = () => { + setSelectedFile(null); + setSelectedEngine(undefined); + onReset(); + }; + return ( @@ -45,19 +75,52 @@ export const UploadBinaryDialog = ({ Select Binary File {}} + onFileSelect={handleFileSelect} isUploading={isUploading} error={uploadError} - onReset={onReset} + onReset={handleReset} accept=".exe,.com,.dll,.bin,.elf" maxSize={100 * 1024 * 1024} + autoUpload={false} /> + {selectedFile && ( +
+
+
+ 2 +
+ Analysis Engine + (optional) +
+ +
+ )} + + {selectedFile && ( + + )}
diff --git a/src/features/project/hooks/useProjectPage.ts b/src/features/project/hooks/useProjectPage.ts index e92b1b5..ea30270 100644 --- a/src/features/project/hooks/useProjectPage.ts +++ b/src/features/project/hooks/useProjectPage.ts @@ -4,6 +4,7 @@ import { useGetProject } from "@/api/generated/project/project.ts"; import { useGetWorkspace } from "@/api/generated/workspace/workspace.ts"; import { useDeleteBinary, useGetBinaries, useUploadBinary } from "@/api/generated/binary/binary.ts"; import { downloadBinary } from "@/api/generated/binary/binary.ts"; +import { useListEngines } from "@/api/generated/engine/engine.ts"; import { useQueryClient } from "@tanstack/react-query"; import type { BinaryResponse } from "@/api/generated/api.schemas.ts"; @@ -28,6 +29,8 @@ export function useProjectPage(projectId: string) { { query: { enabled: !!projectId } } ); + const { data: engines, isLoading: enginesLoading } = useListEngines(); + const { mutate: mutateDeleteBinary } = useDeleteBinary({ mutation: { onSuccess: () => { @@ -69,8 +72,8 @@ export function useProjectPage(projectId: string) { setBinaryToDelete(null); }; - const handleUploadBinary = (file: File) => { - uploadMutation.mutate({ projectId, data: { file } }); + const handleUploadBinary = (file: File, engine?: string) => { + uploadMutation.mutate({ projectId, data: { file }, params: engine ? { engine } : undefined }); }; const handleDownloadBinary = async (binary: BinaryResponse) => { @@ -104,5 +107,7 @@ export function useProjectPage(projectId: string) { uploadError, handleUploadBinary, handleDownloadBinary, + engines: engines ?? [], + enginesLoading, }; } diff --git a/src/pages/Binary.tsx b/src/pages/Binary.tsx index 3173328..744655e 100644 --- a/src/pages/Binary.tsx +++ b/src/pages/Binary.tsx @@ -1,7 +1,6 @@ import { ArrowLeft } from "lucide-react"; import { Button } from "@/components/ui/button"; import { EmptyState } from "@/components/EmptyState"; -import { ConfirmDialog } from "@/components/ConfirmDialog"; import { Breadcrumbs } from "@/components/Breadcrumbs.tsx"; import { BinaryHeader } from "@/features/binary/BinaryHeader.tsx"; import { FunctionTree } from "@/features/binary/FunctionTree"; @@ -14,6 +13,7 @@ import { 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"; const Binary = () => { const { @@ -39,11 +39,12 @@ const Binary = () => { selectedFunction, selectedFunctionLoading, analyzeMutation, - confirmReanalyzeOpen, - setConfirmReanalyzeOpen, - handleAnalyze, - handleReanalyzeRequest, - handleReanalyzeConfirm, + analyzeDialogOpen, + setAnalyzeDialogOpen, + handleOpenAnalyzeDialog, + handleAnalyzeConfirm, + engines, + enginesLoading, } = useBinaryPage(); const navigateToEntryPoint = () => { @@ -91,7 +92,7 @@ const Binary = () => { binary={binary} hasAnalysis={binary.staticAnalysisDone} isAnalyzing={analyzeMutation.isPending} - onAnalyze={binary.staticAnalysisDone ? handleReanalyzeRequest : handleAnalyze} + onAnalyze={handleOpenAnalyzeDialog} entryPoint={analysis?.entryPoint} onNavigateToEntryPoint={navigateToEntryPoint} /> @@ -165,14 +166,14 @@ const Binary = () => { - ); diff --git a/src/pages/Project.tsx b/src/pages/Project.tsx index c030190..dd5693a 100644 --- a/src/pages/Project.tsx +++ b/src/pages/Project.tsx @@ -32,6 +32,8 @@ const Project = () => { uploadError, handleUploadBinary, handleDownloadBinary, + engines, + enginesLoading, } = useProjectPage(projectId!); if (!project || !workspace) { @@ -111,12 +113,15 @@ const Project = () => { )}