feat(binary): add static analysis engine support
This commit is contained in:
parent
011dbe9321
commit
199345d497
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,6 +5,7 @@ target/
|
||||
|
||||
# Uploaded binaries
|
||||
uploads/
|
||||
engines/ida5/ida5_files/**
|
||||
|
||||
### Intellij ###
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
|
||||
2
engines/ghidra/Dockerfile
Normal file
2
engines/ghidra/Dockerfile
Normal file
@ -0,0 +1,2 @@
|
||||
FROM alpine:latest
|
||||
ENTRYPOINT ["/bin/sh", "-c", "echo 'Ghidra engine not yet implemented' && exit 1"]
|
||||
3
engines/ghidra/entrypoint.sh
Normal file
3
engines/ghidra/entrypoint.sh
Normal file
@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
echo "[ERROR] Ghidra engine is not yet implemented."
|
||||
exit 1
|
||||
43
engines/ida5/Dockerfile.ida5
Normal file
43
engines/ida5/Dockerfile.ida5
Normal file
@ -0,0 +1,43 @@
|
||||
FROM debian:bullseye-slim
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install 32-bit Wine architecture and minimal virtual framebuffer (Xvfb)
|
||||
RUN dpkg --add-architecture i386 && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
wine \
|
||||
wine32 \
|
||||
xvfb \
|
||||
xauth && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Configure Wine environment variables
|
||||
ENV WINEPREFIX=/wine/prefix
|
||||
ENV WINEARCH=win32
|
||||
ENV WINEDLLOVERRIDES="mscoree,mshtml="
|
||||
ENV PATH="/usr/lib/wine:${PATH}"
|
||||
|
||||
RUN mkdir -p /wine/prefix
|
||||
|
||||
# Initialize the default Wine prefix in background
|
||||
RUN xvfb-run -a wine wineboot -u
|
||||
|
||||
# Inject the validated DataRescue IDA 5 license key into the Wine registry
|
||||
COPY ida_licenca.reg /tmp/ida_licenca.reg
|
||||
RUN xvfb-run -a wine regedit /tmp/ida_licenca.reg
|
||||
|
||||
# Set up application directories
|
||||
RUN mkdir -p /app/ida /input /output
|
||||
COPY ./ida5_files /app/ida
|
||||
WORKDIR /app/ida
|
||||
|
||||
# Embed the analysis script into the image
|
||||
COPY extract.idc /app/ida/extract.idc
|
||||
|
||||
# Configure the entrypoint script
|
||||
COPY entrypoint.sh /app/ida/entrypoint.sh
|
||||
RUN chmod +x /app/ida/entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/app/ida/entrypoint.sh"]
|
||||
43
engines/ida5/entrypoint.sh
Normal file
43
engines/ida5/entrypoint.sh
Normal file
@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
|
||||
export WINEPREFIX="/wine/prefix"
|
||||
export WINEARCH=win32
|
||||
export WINEDEBUG=-all
|
||||
|
||||
# Accept target filename as first argument
|
||||
TARGET_FILENAME="${1:-target.exe}"
|
||||
|
||||
# Ensure required input files exist in the mounted volumes
|
||||
if [ ! -f "/input/${TARGET_FILENAME}" ]; then
|
||||
echo "[ERROR] Missing /input/${TARGET_FILENAME}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "/app/ida/extract.idc" ]; then
|
||||
echo "[ERROR] Missing /app/ida/extract.idc"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[INFO] Preparing internal analysis environment..."
|
||||
cp "/input/${TARGET_FILENAME}" "/app/ida/target.exe"
|
||||
|
||||
# Force IDA to suppress all initial compiler dialogs and welcome screens
|
||||
echo -e "ASK_COMPILER = NO\nASK_EXIT = NO\nDISPLAY_WELCOME = NO" >> /app/ida/ida.cfg
|
||||
|
||||
echo "[INFO] Launching IDA Pro 5 in headless batch mode..."
|
||||
xvfb-run -a wine idag.exe -A -c -S"extract.idc" "target.exe"
|
||||
|
||||
echo "[INFO] Verifying generated artifacts..."
|
||||
if [ -f "/app/ida/output.json" ]; then
|
||||
cp /app/ida/output.json /output/output.json
|
||||
echo "[SUCCESS] Static analysis completed. output.json is ready!"
|
||||
RET_CODE=0
|
||||
else
|
||||
echo "[ERROR] IDC script failed to generate output.json"
|
||||
RET_CODE=1
|
||||
fi
|
||||
|
||||
# Clean up temporary files inside the container before exiting
|
||||
rm -f /app/ida/target.exe /app/ida/target.idb /app/ida/output.json
|
||||
|
||||
exit $RET_CODE
|
||||
93
engines/ida5/extract.idc
Normal file
93
engines/ida5/extract.idc
Normal file
@ -0,0 +1,93 @@
|
||||
#include <idc.idc>
|
||||
|
||||
static main() {
|
||||
auto ea, func_end, curr_inst, func_name, disasm, file;
|
||||
auto last_inst, xref, ref, label_name;
|
||||
|
||||
Message("Aguardando fim da analise...\n");
|
||||
Wait();
|
||||
|
||||
file = fopen("output.json", "w");
|
||||
if (file == 0) {
|
||||
Message("Erro ao criar arquivo de output!\n");
|
||||
Exit(1);
|
||||
}
|
||||
|
||||
fprintf(file, "[\n");
|
||||
ea = NextFunction(0);
|
||||
|
||||
while (ea != BADADDR) {
|
||||
func_name = GetFunctionName(ea);
|
||||
|
||||
fprintf(file, " {\n");
|
||||
fprintf(file, " \"function\": \"%s\",\n", func_name);
|
||||
fprintf(file, " \"address\": \"0x%08X\",\n", ea);
|
||||
|
||||
// --- XREFS TO (Simplificado e direto para o IDA 5) ---
|
||||
fprintf(file, " \"xrefs\": \"");
|
||||
xref = RfirstB(ea);
|
||||
while (xref != BADADDR) {
|
||||
if (GetFunctionName(xref) != "") {
|
||||
fprintf(file, "%s ", GetFunctionName(xref));
|
||||
}
|
||||
xref = RnextB(ea, xref);
|
||||
}
|
||||
fprintf(file, "\",\n");
|
||||
|
||||
// --- ASSEMBLY BRUTO (com offset) ---
|
||||
fprintf(file, " \"assembly\": \"");
|
||||
func_end = FindFuncEnd(ea);
|
||||
curr_inst = ea;
|
||||
last_inst = 0;
|
||||
|
||||
while (curr_inst < func_end) {
|
||||
if (curr_inst == last_inst) {
|
||||
break;
|
||||
}
|
||||
last_inst = curr_inst;
|
||||
|
||||
disasm = GetDisasm(curr_inst);
|
||||
if (disasm != "") {
|
||||
fprintf(file, "0x%08X %s\\n", curr_inst, disasm);
|
||||
}
|
||||
|
||||
curr_inst = curr_inst + ItemSize(curr_inst);
|
||||
}
|
||||
fprintf(file, "\",\n");
|
||||
|
||||
// --- LABELS (code references from instructions) ---
|
||||
fprintf(file, " \"labels\": \"");
|
||||
curr_inst = ea;
|
||||
last_inst = 0;
|
||||
|
||||
while (curr_inst < func_end) {
|
||||
if (curr_inst == last_inst) {
|
||||
break;
|
||||
}
|
||||
last_inst = curr_inst;
|
||||
|
||||
ref = Rfirst(curr_inst);
|
||||
while (ref != BADADDR) {
|
||||
label_name = Name(ref);
|
||||
if (label_name != "") {
|
||||
fprintf(file, "%s:0x%08X ", label_name, ref);
|
||||
}
|
||||
ref = Rnext(curr_inst, ref);
|
||||
}
|
||||
|
||||
curr_inst = curr_inst + ItemSize(curr_inst);
|
||||
}
|
||||
fprintf(file, "\"\n }");
|
||||
|
||||
ea = NextFunction(ea);
|
||||
if (ea != BADADDR) {
|
||||
fprintf(file, ",\n");
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(file, "\n]\n");
|
||||
fclose(file);
|
||||
|
||||
Message("Extracao concluida com sucesso!\n");
|
||||
Exit(0);
|
||||
}
|
||||
12
engines/ida5/ida_licenca.reg
Normal file
12
engines/ida5/ida_licenca.reg
Normal file
@ -0,0 +1,12 @@
|
||||
Windows Registry Editor Version 5.00
|
||||
|
||||
[HKEY_CURRENT_USER\Software\Hex-Rays\IDA]
|
||||
"LicenseIda5"="I AGREE"
|
||||
|
||||
[HKEY_CURRENT_USER\Software\DataRescue\IDA Pro]
|
||||
"License"="I AGREE"
|
||||
|
||||
[HKEY_CURRENT_USER\Software\DataRescue]
|
||||
|
||||
[HKEY_CURRENT_USER\Software\DataRescue\IDA]
|
||||
"License Mach EDV Dienstleistungen, Jan Mach, 1 user, adv, 11_2007"=dword:00000001
|
||||
@ -0,0 +1,49 @@
|
||||
package ai.decompile.analysis.controller;
|
||||
|
||||
import ai.decompile.analysis.model.dto.AnalysisResponse;
|
||||
import ai.decompile.analysis.model.dto.FunctionSummaryResponse;
|
||||
import ai.decompile.analysis.service.AnalysisService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class AnalysisController {
|
||||
private final AnalysisService analysisService;
|
||||
|
||||
@Operation(
|
||||
summary = "List static analyses for a binary",
|
||||
description = "Retrieve all static analyses performed on a binary.",
|
||||
tags = {"Analysis"},
|
||||
operationId = "listAnalyses")
|
||||
@GetMapping("/binaries/{binaryId}/analysis")
|
||||
public List<AnalysisResponse> listAnalyses(@PathVariable UUID binaryId) {
|
||||
return analysisService.listAnalyses(binaryId);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Get static analysis details",
|
||||
description = "Retrieve details of a specific static analysis.",
|
||||
tags = {"Analysis"},
|
||||
operationId = "getAnalysis")
|
||||
@GetMapping("/analysis/{analysisId}")
|
||||
public AnalysisResponse getAnalysis(@PathVariable UUID analysisId) {
|
||||
return analysisService.getAnalysis(analysisId);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "List functions in an analysis",
|
||||
description = "Retrieve paginated list of functions extracted during a static analysis.",
|
||||
tags = {"Analysis"},
|
||||
operationId = "listFunctions")
|
||||
@GetMapping("/analysis/{analysisId}/functions")
|
||||
public Page<FunctionSummaryResponse> listFunctions(
|
||||
@PathVariable UUID analysisId, Pageable pageable) {
|
||||
return analysisService.listFunctions(analysisId, pageable);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package ai.decompile.analysis.controller;
|
||||
|
||||
import ai.decompile.analysis.model.dto.FunctionDetailResponse;
|
||||
import ai.decompile.analysis.model.dto.XrefResponse;
|
||||
import ai.decompile.analysis.service.AnalysisService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
public class FunctionController {
|
||||
private final AnalysisService analysisService;
|
||||
|
||||
@Operation(
|
||||
summary = "Get function details",
|
||||
description =
|
||||
"Retrieve full details of a function including assembly, decompiled code, and xref counts.",
|
||||
tags = {"Function"},
|
||||
operationId = "getFunction")
|
||||
@GetMapping("/functions/{functionId}")
|
||||
public FunctionDetailResponse getFunction(@PathVariable UUID functionId) {
|
||||
return analysisService.getFunction(functionId);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "List callers of a function",
|
||||
description = "Retrieve functions that call this function (incoming references).",
|
||||
tags = {"Function"},
|
||||
operationId = "getCallers")
|
||||
@GetMapping("/functions/{functionId}/callers")
|
||||
public List<XrefResponse> getCallers(@PathVariable UUID functionId) {
|
||||
return analysisService.getCallers(functionId);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "List callees of a function",
|
||||
description = "Retrieve functions that this function calls (outgoing references).",
|
||||
tags = {"Function"},
|
||||
operationId = "getCallees")
|
||||
@GetMapping("/functions/{functionId}/callees")
|
||||
public List<XrefResponse> getCallees(@PathVariable UUID functionId) {
|
||||
return analysisService.getCallees(functionId);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
package ai.decompile.analysis.controller;
|
||||
@ -0,0 +1,6 @@
|
||||
package ai.decompile.analysis.event;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record StaticAnalysisRequestedEvent(
|
||||
UUID binaryId, UUID projectId, UUID workspaceId, String engineName) {}
|
||||
@ -0,0 +1,2 @@
|
||||
@org.springframework.modulith.NamedInterface("events")
|
||||
package ai.decompile.analysis.event;
|
||||
@ -0,0 +1,27 @@
|
||||
package ai.decompile.analysis.model.dto;
|
||||
|
||||
import ai.decompile.analysis.model.entity.StaticAnalysis;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
public record AnalysisResponse(
|
||||
UUID id,
|
||||
UUID binaryId,
|
||||
UUID jobId,
|
||||
String engine,
|
||||
String status,
|
||||
int functionCount,
|
||||
Instant createdAt,
|
||||
Instant updatedAt) {
|
||||
public static AnalysisResponse from(StaticAnalysis a) {
|
||||
return new AnalysisResponse(
|
||||
a.getId(),
|
||||
a.getBinary().getId(),
|
||||
a.getJobId(),
|
||||
a.getEngine(),
|
||||
a.getStatus(),
|
||||
a.getFunctionCount(),
|
||||
a.getCreatedAt(),
|
||||
a.getUpdatedAt());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package ai.decompile.analysis.model.dto;
|
||||
|
||||
import ai.decompile.analysis.model.entity.StaticFunction;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public record FunctionDetailResponse(
|
||||
UUID id,
|
||||
UUID analysisId,
|
||||
String name,
|
||||
String address,
|
||||
String assembly,
|
||||
String decompiledCode,
|
||||
String signature,
|
||||
int callerCount,
|
||||
int calleeCount,
|
||||
List<LabelResponse> labels) {
|
||||
public static FunctionDetailResponse from(
|
||||
StaticFunction f, int callerCount, int calleeCount, List<LabelResponse> labels) {
|
||||
return new FunctionDetailResponse(
|
||||
f.getId(),
|
||||
f.getAnalysis().getId(),
|
||||
f.getName(),
|
||||
f.getAddress(),
|
||||
f.getAssembly(),
|
||||
f.getDecompiledCode(),
|
||||
f.getSignature(),
|
||||
callerCount,
|
||||
calleeCount,
|
||||
labels);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package ai.decompile.analysis.model.dto;
|
||||
|
||||
import ai.decompile.analysis.model.entity.StaticFunction;
|
||||
import java.util.UUID;
|
||||
|
||||
public record FunctionSummaryResponse(UUID id, UUID analysisId, String name, String address) {
|
||||
public static FunctionSummaryResponse from(StaticFunction f) {
|
||||
return new FunctionSummaryResponse(
|
||||
f.getId(), f.getAnalysis().getId(), f.getName(), f.getAddress());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package ai.decompile.analysis.model.dto;
|
||||
|
||||
import ai.decompile.analysis.model.entity.StaticLabel;
|
||||
|
||||
public record LabelResponse(String name, String address) {
|
||||
public static LabelResponse from(StaticLabel l) {
|
||||
return new LabelResponse(l.getName(), l.getAddress());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package ai.decompile.analysis.model.dto;
|
||||
|
||||
import ai.decompile.analysis.model.entity.StaticXref;
|
||||
import java.util.UUID;
|
||||
|
||||
public record XrefResponse(
|
||||
UUID id,
|
||||
UUID callerId,
|
||||
String callerName,
|
||||
UUID calleeId,
|
||||
String calleeName,
|
||||
String type,
|
||||
String address) {
|
||||
public static XrefResponse from(StaticXref xref) {
|
||||
return new XrefResponse(
|
||||
xref.getId(),
|
||||
xref.getCaller().getId(),
|
||||
xref.getCaller().getName(),
|
||||
xref.getCallee().getId(),
|
||||
xref.getCallee().getName(),
|
||||
xref.getType(),
|
||||
xref.getAddress());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
@org.springframework.modulith.NamedInterface("dto")
|
||||
package ai.decompile.analysis.model.dto;
|
||||
@ -0,0 +1,57 @@
|
||||
package ai.decompile.analysis.model.entity;
|
||||
|
||||
import ai.decompile.workspace.model.entity.Binary;
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
@Entity
|
||||
@Table(name = "static_analysis")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class StaticAnalysis {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "binary_id", nullable = false)
|
||||
private Binary binary;
|
||||
|
||||
@Column(name = "job_id")
|
||||
private UUID jobId;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
private String engine;
|
||||
|
||||
@Column(nullable = false, length = 50)
|
||||
private String status;
|
||||
|
||||
@Column(name = "function_count", nullable = false)
|
||||
@Builder.Default
|
||||
private int functionCount = 0;
|
||||
|
||||
@OneToMany(
|
||||
mappedBy = "analysis",
|
||||
cascade = CascadeType.ALL,
|
||||
orphanRemoval = true,
|
||||
fetch = FetchType.LAZY)
|
||||
@Builder.Default
|
||||
private List<StaticFunction> functions = new ArrayList<>();
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Instant updatedAt;
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
package ai.decompile.analysis.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
@Entity
|
||||
@Table(name = "static_function")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class StaticFunction {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "analysis_id", nullable = false)
|
||||
private StaticAnalysis analysis;
|
||||
|
||||
@Column(nullable = false, length = 500)
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
private String address;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String assembly;
|
||||
|
||||
@Column(name = "decompiled_code", columnDefinition = "TEXT")
|
||||
private String decompiledCode;
|
||||
|
||||
@Column(length = 1000)
|
||||
private String signature;
|
||||
|
||||
@OneToMany(
|
||||
mappedBy = "caller",
|
||||
cascade = CascadeType.ALL,
|
||||
orphanRemoval = true,
|
||||
fetch = FetchType.LAZY)
|
||||
@Builder.Default
|
||||
private List<StaticXref> calleeXrefs = new ArrayList<>();
|
||||
|
||||
@OneToMany(
|
||||
mappedBy = "callee",
|
||||
cascade = CascadeType.ALL,
|
||||
orphanRemoval = true,
|
||||
fetch = FetchType.LAZY)
|
||||
@Builder.Default
|
||||
private List<StaticXref> callerXrefs = new ArrayList<>();
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package ai.decompile.analysis.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.UUID;
|
||||
import lombok.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "static_label")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class StaticLabel {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "function_id", nullable = false)
|
||||
private StaticFunction function;
|
||||
|
||||
@Column(nullable = false, length = 500)
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
private String address;
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package ai.decompile.analysis.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.UUID;
|
||||
import lombok.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "static_xref")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class StaticXref {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "caller_id", nullable = false)
|
||||
private StaticFunction caller;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "callee_id", nullable = false)
|
||||
private StaticFunction callee;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
@Builder.Default
|
||||
private String type = "CALL";
|
||||
|
||||
@Column(length = 20)
|
||||
private String address;
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
@org.springframework.modulith.NamedInterface("entities")
|
||||
package ai.decompile.analysis.model.entity;
|
||||
@ -0,0 +1,13 @@
|
||||
package ai.decompile.analysis.model.repository;
|
||||
|
||||
import ai.decompile.analysis.model.entity.StaticAnalysis;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface StaticAnalysisRepository extends JpaRepository<StaticAnalysis, UUID> {
|
||||
List<StaticAnalysis> findByBinaryIdOrderByCreatedAtDesc(UUID binaryId);
|
||||
|
||||
Optional<StaticAnalysis> findByBinaryIdAndEngine(UUID binaryId, String engine);
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package ai.decompile.analysis.model.repository;
|
||||
|
||||
import ai.decompile.analysis.model.entity.StaticFunction;
|
||||
import java.util.UUID;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface StaticFunctionRepository extends JpaRepository<StaticFunction, UUID> {
|
||||
Page<StaticFunction> findByAnalysisId(UUID analysisId, Pageable pageable);
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package ai.decompile.analysis.model.repository;
|
||||
|
||||
import ai.decompile.analysis.model.entity.StaticLabel;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface StaticLabelRepository extends JpaRepository<StaticLabel, UUID> {
|
||||
List<StaticLabel> findByFunctionId(UUID functionId);
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package ai.decompile.analysis.model.repository;
|
||||
|
||||
import ai.decompile.analysis.model.entity.StaticXref;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface StaticXrefRepository extends JpaRepository<StaticXref, UUID> {
|
||||
List<StaticXref> findByCallerId(UUID callerId);
|
||||
|
||||
List<StaticXref> findByCalleeId(UUID calleeId);
|
||||
|
||||
int countByCallerId(UUID callerId);
|
||||
|
||||
int countByCalleeId(UUID calleeId);
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
package ai.decompile.analysis.model.repository;
|
||||
4
src/main/java/ai/decompile/analysis/package-info.java
Normal file
4
src/main/java/ai/decompile/analysis/package-info.java
Normal file
@ -0,0 +1,4 @@
|
||||
@org.springframework.modulith.ApplicationModule(
|
||||
displayName = "Analysis",
|
||||
allowedDependencies = {"workspace::entities", "engine::model", "common"})
|
||||
package ai.decompile.analysis;
|
||||
191
src/main/java/ai/decompile/analysis/service/AnalysisService.java
Normal file
191
src/main/java/ai/decompile/analysis/service/AnalysisService.java
Normal file
@ -0,0 +1,191 @@
|
||||
package ai.decompile.analysis.service;
|
||||
|
||||
import ai.decompile.analysis.model.dto.AnalysisResponse;
|
||||
import ai.decompile.analysis.model.dto.FunctionDetailResponse;
|
||||
import ai.decompile.analysis.model.dto.FunctionSummaryResponse;
|
||||
import ai.decompile.analysis.model.dto.LabelResponse;
|
||||
import ai.decompile.analysis.model.dto.XrefResponse;
|
||||
import ai.decompile.analysis.model.entity.StaticAnalysis;
|
||||
import ai.decompile.analysis.model.entity.StaticFunction;
|
||||
import ai.decompile.analysis.model.entity.StaticLabel;
|
||||
import ai.decompile.analysis.model.entity.StaticXref;
|
||||
import ai.decompile.analysis.model.repository.StaticAnalysisRepository;
|
||||
import ai.decompile.analysis.model.repository.StaticFunctionRepository;
|
||||
import ai.decompile.analysis.model.repository.StaticLabelRepository;
|
||||
import ai.decompile.analysis.model.repository.StaticXrefRepository;
|
||||
import ai.decompile.common.exception.NotFoundException;
|
||||
import ai.decompile.engine.model.AnalysisResult;
|
||||
import ai.decompile.workspace.model.entity.Binary;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Log4j2
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AnalysisService {
|
||||
private final StaticAnalysisRepository analysisRepository;
|
||||
private final StaticFunctionRepository functionRepository;
|
||||
private final StaticXrefRepository xrefRepository;
|
||||
private final StaticLabelRepository labelRepository;
|
||||
|
||||
@Transactional
|
||||
public AnalysisResponse saveResult(
|
||||
Binary binary, UUID jobId, String engine, AnalysisResult result) {
|
||||
replaceExistingAnalysis(binary, engine);
|
||||
|
||||
var analysis = createAnalysis(binary, jobId, engine, result);
|
||||
var nameToId = saveFunctions(analysis, result);
|
||||
saveXrefs(result, nameToId);
|
||||
|
||||
return AnalysisResponse.from(analysis);
|
||||
}
|
||||
|
||||
public List<AnalysisResponse> listAnalyses(UUID binaryId) {
|
||||
return analysisRepository.findByBinaryIdOrderByCreatedAtDesc(binaryId).stream()
|
||||
.map(AnalysisResponse::from)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public AnalysisResponse getAnalysis(UUID analysisId) {
|
||||
return AnalysisResponse.from(findAnalysis(analysisId));
|
||||
}
|
||||
|
||||
public Page<FunctionSummaryResponse> listFunctions(UUID analysisId, Pageable pageable) {
|
||||
assertAnalysisExists(analysisId);
|
||||
return functionRepository
|
||||
.findByAnalysisId(analysisId, pageable)
|
||||
.map(FunctionSummaryResponse::from);
|
||||
}
|
||||
|
||||
public FunctionDetailResponse getFunction(UUID functionId) {
|
||||
var func = findFunction(functionId);
|
||||
int calleeCount = xrefRepository.countByCallerId(functionId);
|
||||
int callerCount = xrefRepository.countByCalleeId(functionId);
|
||||
var labels =
|
||||
labelRepository.findByFunctionId(functionId).stream().map(LabelResponse::from).toList();
|
||||
return FunctionDetailResponse.from(func, callerCount, calleeCount, labels);
|
||||
}
|
||||
|
||||
public List<XrefResponse> getCallers(UUID functionId) {
|
||||
return listXrefs(functionId, xrefRepository::findByCalleeId);
|
||||
}
|
||||
|
||||
public List<XrefResponse> getCallees(UUID functionId) {
|
||||
return listXrefs(functionId, xrefRepository::findByCallerId);
|
||||
}
|
||||
|
||||
private void replaceExistingAnalysis(Binary binary, String engine) {
|
||||
analysisRepository
|
||||
.findByBinaryIdAndEngine(binary.getId(), engine)
|
||||
.ifPresent(
|
||||
existing -> {
|
||||
log.info(
|
||||
"Deleting existing {} analysis for binary {} (id={})",
|
||||
engine,
|
||||
binary.getId(),
|
||||
existing.getId());
|
||||
analysisRepository.delete(existing);
|
||||
analysisRepository.flush();
|
||||
});
|
||||
}
|
||||
|
||||
private StaticAnalysis createAnalysis(
|
||||
Binary binary, UUID jobId, String engine, AnalysisResult result) {
|
||||
return analysisRepository.save(
|
||||
StaticAnalysis.builder()
|
||||
.binary(binary)
|
||||
.jobId(jobId)
|
||||
.engine(engine)
|
||||
.status("COMPLETED")
|
||||
.functionCount(result.functions().size())
|
||||
.build());
|
||||
}
|
||||
|
||||
private Map<String, UUID> saveFunctions(StaticAnalysis analysis, AnalysisResult result) {
|
||||
var nameToId = new HashMap<String, UUID>();
|
||||
for (var funcResult : result.functions()) {
|
||||
var func =
|
||||
functionRepository.save(
|
||||
StaticFunction.builder()
|
||||
.analysis(analysis)
|
||||
.name(funcResult.name())
|
||||
.address(funcResult.address())
|
||||
.assembly(funcResult.assembly())
|
||||
.decompiledCode(funcResult.decompiledCode())
|
||||
.signature(funcResult.signature())
|
||||
.build());
|
||||
nameToId.put(funcResult.name(), func.getId());
|
||||
|
||||
for (var label : funcResult.labels()) {
|
||||
labelRepository.save(
|
||||
StaticLabel.builder()
|
||||
.function(func)
|
||||
.name(label.name())
|
||||
.address(label.address())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
return nameToId;
|
||||
}
|
||||
|
||||
private void saveXrefs(AnalysisResult result, Map<String, UUID> nameToId) {
|
||||
for (var funcResult : result.functions()) {
|
||||
var callerId = nameToId.get(funcResult.name());
|
||||
if (callerId == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var calleeName : funcResult.xrefNames()) {
|
||||
var calleeId = nameToId.get(calleeName);
|
||||
if (calleeId == null) {
|
||||
log.debug(
|
||||
"Skipping xref from '{}' to unknown function '{}' (likely external/lib)",
|
||||
funcResult.name(),
|
||||
calleeName);
|
||||
continue;
|
||||
}
|
||||
|
||||
xrefRepository.save(
|
||||
StaticXref.builder()
|
||||
.caller(functionRepository.getReferenceById(callerId))
|
||||
.callee(functionRepository.getReferenceById(calleeId))
|
||||
.type("CALL")
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<XrefResponse> listXrefs(UUID functionId, Function<UUID, List<StaticXref>> finder) {
|
||||
findFunction(functionId);
|
||||
return finder.apply(functionId).stream().map(XrefResponse::from).toList();
|
||||
}
|
||||
|
||||
private void assertAnalysisExists(UUID analysisId) {
|
||||
if (!analysisRepository.existsById(analysisId)) {
|
||||
throw new NotFoundException("Static analysis with id '" + analysisId + "' not found");
|
||||
}
|
||||
}
|
||||
|
||||
private StaticAnalysis findAnalysis(UUID analysisId) {
|
||||
return analysisRepository
|
||||
.findById(analysisId)
|
||||
.orElseThrow(
|
||||
() -> new NotFoundException("Static analysis with id '" + analysisId + "' not found"));
|
||||
}
|
||||
|
||||
private StaticFunction findFunction(UUID functionId) {
|
||||
return functionRepository
|
||||
.findById(functionId)
|
||||
.orElseThrow(
|
||||
() -> new NotFoundException("Function with id '" + functionId + "' not found"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
@org.springframework.modulith.NamedInterface("services")
|
||||
package ai.decompile.analysis.service;
|
||||
@ -0,0 +1,29 @@
|
||||
package ai.decompile.engine.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "app")
|
||||
public class EngineProperties {
|
||||
private Map<String, EngineConfig> engines = new HashMap<>();
|
||||
|
||||
public EngineConfig getEngineConfig(String engineName) {
|
||||
var config = engines.get(engineName);
|
||||
if (config == null) {
|
||||
throw new IllegalArgumentException("No configuration found for engine '" + engineName + "'");
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class EngineConfig {
|
||||
private String image;
|
||||
private long timeoutSeconds = 300;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
package ai.decompile.engine.config;
|
||||
@ -0,0 +1,5 @@
|
||||
package ai.decompile.engine.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record AnalysisResult(List<FunctionResult> functions) {}
|
||||
6
src/main/java/ai/decompile/engine/model/EngineType.java
Normal file
6
src/main/java/ai/decompile/engine/model/EngineType.java
Normal file
@ -0,0 +1,6 @@
|
||||
package ai.decompile.engine.model;
|
||||
|
||||
public enum EngineType {
|
||||
IDA5,
|
||||
GHIDRA
|
||||
}
|
||||
12
src/main/java/ai/decompile/engine/model/FunctionResult.java
Normal file
12
src/main/java/ai/decompile/engine/model/FunctionResult.java
Normal file
@ -0,0 +1,12 @@
|
||||
package ai.decompile.engine.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record FunctionResult(
|
||||
String name,
|
||||
String address,
|
||||
String assembly,
|
||||
String decompiledCode,
|
||||
String signature,
|
||||
List<String> xrefNames,
|
||||
List<LabelEntry> labels) {}
|
||||
3
src/main/java/ai/decompile/engine/model/LabelEntry.java
Normal file
3
src/main/java/ai/decompile/engine/model/LabelEntry.java
Normal file
@ -0,0 +1,3 @@
|
||||
package ai.decompile.engine.model;
|
||||
|
||||
public record LabelEntry(String name, String address) {}
|
||||
@ -0,0 +1,2 @@
|
||||
@org.springframework.modulith.NamedInterface("model")
|
||||
package ai.decompile.engine.model;
|
||||
4
src/main/java/ai/decompile/engine/package-info.java
Normal file
4
src/main/java/ai/decompile/engine/package-info.java
Normal file
@ -0,0 +1,4 @@
|
||||
@org.springframework.modulith.ApplicationModule(
|
||||
displayName = "Engine",
|
||||
allowedDependencies = {"docker", "common"})
|
||||
package ai.decompile.engine;
|
||||
10
src/main/java/ai/decompile/engine/service/EngineService.java
Normal file
10
src/main/java/ai/decompile/engine/service/EngineService.java
Normal file
@ -0,0 +1,10 @@
|
||||
package ai.decompile.engine.service;
|
||||
|
||||
import ai.decompile.engine.model.AnalysisResult;
|
||||
import ai.decompile.engine.model.EngineType;
|
||||
|
||||
public interface EngineService {
|
||||
AnalysisResult analyze(String filePath);
|
||||
|
||||
EngineType supportedEngine();
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package ai.decompile.engine.service;
|
||||
|
||||
import ai.decompile.engine.model.AnalysisResult;
|
||||
import ai.decompile.engine.model.EngineType;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class GhidraEngineService implements EngineService {
|
||||
@Override
|
||||
public AnalysisResult analyze(String filePath) {
|
||||
throw new UnsupportedOperationException("Ghidra engine is not yet implemented.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public EngineType supportedEngine() {
|
||||
return EngineType.GHIDRA;
|
||||
}
|
||||
}
|
||||
222
src/main/java/ai/decompile/engine/service/Ida5EngineService.java
Normal file
222
src/main/java/ai/decompile/engine/service/Ida5EngineService.java
Normal file
@ -0,0 +1,222 @@
|
||||
package ai.decompile.engine.service;
|
||||
|
||||
import ai.decompile.docker.service.ContainerService;
|
||||
import ai.decompile.engine.config.EngineProperties;
|
||||
import ai.decompile.engine.model.AnalysisResult;
|
||||
import ai.decompile.engine.model.EngineType;
|
||||
import ai.decompile.engine.model.FunctionResult;
|
||||
import ai.decompile.engine.model.LabelEntry;
|
||||
import com.github.dockerjava.api.model.Bind;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Log4j2
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(name = "app.docker.enabled", havingValue = "true")
|
||||
public class Ida5EngineService implements EngineService {
|
||||
private static final String DOCKERFILE_PATH = "engines/ida5/Dockerfile.ida5";
|
||||
private static final String KEY_ADDRESS = "\",\n \"address\": \"";
|
||||
private static final String KEY_XREFS = "\",\n \"xrefs\": \"";
|
||||
private static final String KEY_ASSEMBLY = "\",\n \"assembly\": \"";
|
||||
private static final String KEY_LABELS = "\",\n \"labels\": \"";
|
||||
private static final String KEY_ENTRY_END = "\"\n }";
|
||||
|
||||
private final ContainerService containers;
|
||||
private final EngineProperties engineProperties;
|
||||
|
||||
@Override
|
||||
public AnalysisResult analyze(String filePath) {
|
||||
var config = engineProperties.getEngineConfig("ida5");
|
||||
var absolutePath = Path.of(filePath).toAbsolutePath().normalize();
|
||||
var filename = absolutePath.getFileName().toString();
|
||||
|
||||
containers.ensureImage(config.getImage(), new File(DOCKERFILE_PATH));
|
||||
|
||||
Path outputDir = createTempDir();
|
||||
var containerId = createContainer(config.getImage(), filename, absolutePath, outputDir);
|
||||
|
||||
try {
|
||||
var exitCode = containers.waitForExit(containerId, config.getTimeoutSeconds());
|
||||
var logs = containers.captureLogs(containerId, 2);
|
||||
|
||||
validateExitCode(exitCode, logs);
|
||||
var outputFile = validateOutputFile(outputDir, logs);
|
||||
return parseOutput(readStringTolerant(outputFile));
|
||||
} catch (Exception e) {
|
||||
log.error("IDA5 analysis failed for file {}: {}", filePath, e.getMessage());
|
||||
throw new RuntimeException("IDA5 analysis failed: " + e.getMessage(), e);
|
||||
} finally {
|
||||
containers.stopAndRemove(containerId);
|
||||
cleanupTempDir(outputDir);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public EngineType supportedEngine() {
|
||||
return EngineType.IDA5;
|
||||
}
|
||||
|
||||
private static Path createTempDir() {
|
||||
try {
|
||||
return Files.createTempDirectory("ida5-output-");
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to create temporary output directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String createContainer(String image, String filename, Path absolutePath, Path outputDir) {
|
||||
var inputBind = Bind.parse(absolutePath.getParent() + ":/input:ro");
|
||||
var outputBind = Bind.parse(outputDir.toAbsolutePath() + ":/output:rw");
|
||||
|
||||
var containerId =
|
||||
containers.createContainer(image, new String[] {filename}, inputBind, outputBind);
|
||||
containers.startContainer(containerId);
|
||||
return containerId;
|
||||
}
|
||||
|
||||
private static void validateExitCode(int exitCode, String logs) {
|
||||
if (exitCode != 0) {
|
||||
log.error("IDA5 container exited with code {}: {}", exitCode, logs);
|
||||
throw new RuntimeException(
|
||||
"IDA5 analysis failed with exit code " + exitCode + ". Output: " + logs);
|
||||
}
|
||||
}
|
||||
|
||||
private static Path validateOutputFile(Path outputDir, String logs) {
|
||||
var outputFile = outputDir.resolve("output.json");
|
||||
if (!Files.exists(outputFile)) {
|
||||
throw new RuntimeException(
|
||||
"IDA5 analysis did not produce output.json. Container logs: " + logs);
|
||||
}
|
||||
return outputFile;
|
||||
}
|
||||
|
||||
private static void cleanupTempDir(Path dir) {
|
||||
try (var walk = Files.walk(dir)) {
|
||||
walk.sorted(Comparator.reverseOrder()).forEach(Ida5EngineService::deleteSilently);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static void deleteSilently(Path path) {
|
||||
try {
|
||||
Files.deleteIfExists(path);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static String readStringTolerant(Path path) throws IOException {
|
||||
var decoder =
|
||||
StandardCharsets.UTF_8
|
||||
.newDecoder()
|
||||
.onMalformedInput(CodingErrorAction.REPLACE)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPLACE);
|
||||
return decoder.decode(ByteBuffer.wrap(Files.readAllBytes(path))).toString();
|
||||
}
|
||||
|
||||
private static AnalysisResult parseOutput(String raw) {
|
||||
var functions = new ArrayList<FunctionResult>();
|
||||
var text = raw.replace("\r\n", "\n");
|
||||
|
||||
var sections = text.split("\"function\"\\s*:\\s*\"");
|
||||
for (int i = 1; i < sections.length; i++) {
|
||||
var section = sections[i];
|
||||
|
||||
var name = extractField(section, KEY_ADDRESS);
|
||||
if (name == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var remainder = after(section, KEY_ADDRESS);
|
||||
if (remainder == null) {
|
||||
continue;
|
||||
}
|
||||
var address = extractField(remainder, KEY_XREFS);
|
||||
if (address == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
remainder = after(remainder, KEY_XREFS);
|
||||
if (remainder == null) {
|
||||
continue;
|
||||
}
|
||||
var xrefsRaw = extractField(remainder, KEY_ASSEMBLY);
|
||||
if (xrefsRaw == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
remainder = after(remainder, KEY_ASSEMBLY);
|
||||
if (remainder == null) {
|
||||
continue;
|
||||
}
|
||||
var assembly = extractField(remainder, KEY_LABELS);
|
||||
if (assembly == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
remainder = after(remainder, KEY_LABELS);
|
||||
if (remainder == null) {
|
||||
continue;
|
||||
}
|
||||
var labelsRaw = extractField(remainder, KEY_ENTRY_END);
|
||||
if (labelsRaw == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var xrefNames =
|
||||
xrefsRaw.isBlank() ? List.<String>of() : Arrays.asList(xrefsRaw.trim().split("\\s+"));
|
||||
var labels = parseLabels(labelsRaw.trim());
|
||||
functions.add(new FunctionResult(name, address, assembly, null, null, xrefNames, labels));
|
||||
}
|
||||
|
||||
if (functions.isEmpty()) {
|
||||
log.warn(
|
||||
"IDA5 produced no parsable function entries. Raw output (first 500 chars): {}",
|
||||
raw.length() > 500 ? raw.substring(0, 500) : raw);
|
||||
}
|
||||
|
||||
return new AnalysisResult(functions);
|
||||
}
|
||||
|
||||
private static String extractField(String text, String endDelim) {
|
||||
int end = text.indexOf(endDelim);
|
||||
return end < 0 ? null : text.substring(0, end);
|
||||
}
|
||||
|
||||
private static String after(String text, String delim) {
|
||||
int idx = text.indexOf(delim);
|
||||
return idx < 0 ? null : text.substring(idx + delim.length());
|
||||
}
|
||||
|
||||
private static List<LabelEntry> parseLabels(String raw) {
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
return Arrays.stream(raw.trim().split("\\s+"))
|
||||
.distinct()
|
||||
.map(Ida5EngineService::parseLabelEntry)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static LabelEntry parseLabelEntry(String token) {
|
||||
int colon = token.lastIndexOf(':');
|
||||
if (colon < 0) {
|
||||
return new LabelEntry(token, "");
|
||||
}
|
||||
return new LabelEntry(token.substring(0, colon), token.substring(colon + 1));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
@org.springframework.modulith.NamedInterface("services")
|
||||
package ai.decompile.engine.service;
|
||||
@ -1,5 +1,6 @@
|
||||
package ai.decompile.job.model.enums;
|
||||
|
||||
public enum JobType {
|
||||
ANALYZE_FILE
|
||||
ANALYZE_FILE,
|
||||
STATIC_ANALYSIS
|
||||
}
|
||||
|
||||
@ -6,6 +6,10 @@
|
||||
"workspace::services",
|
||||
"die::service",
|
||||
"die::model",
|
||||
"analysis::events",
|
||||
"analysis::services",
|
||||
"analysis::dto",
|
||||
"engine::services",
|
||||
"common"
|
||||
})
|
||||
package ai.decompile.job;
|
||||
|
||||
@ -13,11 +13,13 @@ import ai.decompile.job.model.specification.JobSpecification;
|
||||
import ai.decompile.workspace.event.BinaryUploadedEvent;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.modulith.events.ApplicationModuleListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Log4j2
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class JobService {
|
||||
@ -66,8 +68,13 @@ public class JobService {
|
||||
|
||||
@ApplicationModuleListener
|
||||
void onBinaryUploaded(BinaryUploadedEvent event) {
|
||||
log.info(
|
||||
"Received BinaryUploadedEvent for binary={} project={}",
|
||||
event.binaryId(),
|
||||
event.projectId());
|
||||
var job = createJob(event);
|
||||
jobMessagePublisher.publish(job.getId());
|
||||
log.info("Published ANALYZE_FILE job {} to RabbitMQ", job.getId());
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
package ai.decompile.job.service;
|
||||
|
||||
import ai.decompile.analysis.event.StaticAnalysisRequestedEvent;
|
||||
import ai.decompile.job.messaging.JobMessagePublisher;
|
||||
import ai.decompile.job.model.entity.Job;
|
||||
import ai.decompile.job.model.enums.JobStatus;
|
||||
import ai.decompile.job.model.enums.JobType;
|
||||
import ai.decompile.job.model.repository.JobRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.modulith.events.ApplicationModuleListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Log4j2
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class StaticAnalysisEventSubscriber {
|
||||
private final JobRepository jobRepository;
|
||||
private final JobMessagePublisher jobMessagePublisher;
|
||||
|
||||
@ApplicationModuleListener
|
||||
public void on(StaticAnalysisRequestedEvent event) {
|
||||
log.info(
|
||||
">>> StaticAnalysisEventSubscriber: Received StaticAnalysisRequestedEvent binary={} engine={}",
|
||||
event.binaryId(),
|
||||
event.engineName());
|
||||
|
||||
var job = createJob(event);
|
||||
|
||||
log.info(
|
||||
">>> StaticAnalysisEventSubscriber: Saved job {} - publishing to RabbitMQ", job.getId());
|
||||
jobMessagePublisher.publish(job.getId());
|
||||
log.info(">>> StaticAnalysisEventSubscriber: Published job {} to RabbitMQ", job.getId());
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
Job createJob(StaticAnalysisRequestedEvent event) {
|
||||
return jobRepository.save(
|
||||
Job.builder()
|
||||
.type(JobType.STATIC_ANALYSIS)
|
||||
.status(JobStatus.ENQUEUED)
|
||||
.projectId(event.projectId())
|
||||
.binaryId(event.binaryId())
|
||||
.payload("{\"engine\":\"" + event.engineName() + "\"}")
|
||||
.retryCount(0)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
package ai.decompile.job.service.handler;
|
||||
|
||||
import ai.decompile.analysis.service.AnalysisService;
|
||||
import ai.decompile.engine.service.EngineService;
|
||||
import ai.decompile.job.model.entity.Job;
|
||||
import ai.decompile.job.model.enums.JobStatus;
|
||||
import ai.decompile.job.model.enums.JobType;
|
||||
import ai.decompile.workspace.service.BinaryService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Log4j2
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "app.docker.enabled", havingValue = "true")
|
||||
public class StaticAnalysisHandler implements JobHandler {
|
||||
private final Map<String, EngineService> engineServices;
|
||||
private final AnalysisService analysisService;
|
||||
private final BinaryService binaryService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public StaticAnalysisHandler(
|
||||
List<EngineService> engineServiceList,
|
||||
AnalysisService analysisService,
|
||||
BinaryService binaryService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.engineServices =
|
||||
engineServiceList.stream()
|
||||
.collect(Collectors.toMap(e -> e.supportedEngine().name(), Function.identity()));
|
||||
this.analysisService = analysisService;
|
||||
this.binaryService = binaryService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Job job) {
|
||||
var engineName = readEngineFromPayload(job.getPayload());
|
||||
log.info(
|
||||
"Processing STATIC_ANALYSIS job {} for binary {} in project {} with engine {}",
|
||||
job.getId(),
|
||||
job.getBinaryId(),
|
||||
job.getProjectId(),
|
||||
engineName);
|
||||
|
||||
var engineService = engineServices.get(engineName);
|
||||
if (engineService == null) {
|
||||
job.setStatus(JobStatus.FAILED);
|
||||
job.setErrorMessage(
|
||||
"Unknown engine '" + engineName + "'. Available: " + engineServices.keySet());
|
||||
return;
|
||||
}
|
||||
|
||||
var filePath = binaryService.getBinaryFilePath(job.getBinaryId());
|
||||
var result = engineService.analyze(filePath);
|
||||
|
||||
var binary = binaryService.getBinary(job.getBinaryId());
|
||||
var analysisResponse = analysisService.saveResult(binary, job.getId(), engineName, result);
|
||||
|
||||
binaryService.markStaticAnalysisDone(job.getBinaryId());
|
||||
|
||||
job.setResult(toJson(Map.of("analysisId", analysisResponse.id().toString())));
|
||||
job.setStatus(JobStatus.COMPLETED);
|
||||
|
||||
log.info(
|
||||
"Job {} completed. engine={}, functionCount={}",
|
||||
job.getId(),
|
||||
engineName,
|
||||
analysisResponse.functionCount());
|
||||
}
|
||||
|
||||
@Override
|
||||
public JobType supportedType() {
|
||||
return JobType.STATIC_ANALYSIS;
|
||||
}
|
||||
|
||||
private String readEngineFromPayload(String payload) {
|
||||
try {
|
||||
var node = objectMapper.readTree(payload);
|
||||
return node.get("engine").asText();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to read engine from job payload", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String toJson(Object obj) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(obj);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to serialize result to JSON", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ import ai.decompile.workspace.service.BinaryService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
@ -38,7 +39,8 @@ public class BinaryController {
|
||||
|
||||
@Operation(
|
||||
summary = "Upload a binary",
|
||||
description = "Upload a binary file to a project.",
|
||||
description =
|
||||
"Upload a binary file to a project. Optionally specify an engine for static analysis.",
|
||||
tags = {"Binary"},
|
||||
operationId = "uploadBinary")
|
||||
@PostMapping(
|
||||
@ -46,8 +48,28 @@ public class BinaryController {
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public BinaryResponse uploadBinary(
|
||||
@PathVariable UUID projectId, @RequestPart MultipartFile file) {
|
||||
return binaryService.uploadBinary(projectId, file);
|
||||
@PathVariable UUID projectId,
|
||||
@RequestPart MultipartFile file,
|
||||
@Parameter(description = "Static analysis engine to use (IDA5, GHIDRA)")
|
||||
@RequestParam(required = false)
|
||||
String engine) {
|
||||
return binaryService.uploadBinary(projectId, file, engine);
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Trigger static analysis on a binary",
|
||||
description =
|
||||
"Trigger or re-trigger static analysis on an existing binary with the specified engine.",
|
||||
tags = {"Binary"},
|
||||
operationId = "analyzeBinary")
|
||||
@PostMapping("/binaries/{binaryId}/analyze")
|
||||
public ResponseEntity<Map<String, String>> analyzeBinary(
|
||||
@PathVariable UUID binaryId,
|
||||
@Parameter(description = "Static analysis engine to use (IDA5, GHIDRA)") @RequestParam
|
||||
String engine) {
|
||||
binaryService.requestStaticAnalysis(binaryId, engine);
|
||||
return ResponseEntity.accepted()
|
||||
.body(Map.of("message", "Static analysis requested with engine " + engine));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
|
||||
@ -15,6 +15,7 @@ public record BinaryResponse(
|
||||
String architecture,
|
||||
String compiler,
|
||||
@NotBlank String filePath,
|
||||
boolean staticAnalysisDone,
|
||||
@NotNull Instant updatedAt) {
|
||||
public static BinaryResponse from(Binary binary) {
|
||||
return new BinaryResponse(
|
||||
@ -26,6 +27,7 @@ public record BinaryResponse(
|
||||
binary.getArchitecture(),
|
||||
binary.getCompiler(),
|
||||
binary.getFilePath(),
|
||||
binary.isStaticAnalysisDone(),
|
||||
binary.getUpdatedAt());
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
package ai.decompile.workspace.model.entity;
|
||||
|
||||
import ai.decompile.analysis.model.entity.StaticAnalysis;
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
@ -41,6 +44,18 @@ public class Binary {
|
||||
@JoinColumn(name = "project_id", nullable = false)
|
||||
private Project project;
|
||||
|
||||
@Column(name = "static_analysis_done", nullable = false)
|
||||
@Builder.Default
|
||||
private boolean staticAnalysisDone = false;
|
||||
|
||||
@OneToMany(
|
||||
mappedBy = "binary",
|
||||
cascade = CascadeType.ALL,
|
||||
orphanRemoval = true,
|
||||
fetch = FetchType.LAZY)
|
||||
@Builder.Default
|
||||
private List<StaticAnalysis> staticAnalyses = new ArrayList<>();
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
@org.springframework.modulith.ApplicationModule(displayName = "Workspace")
|
||||
@org.springframework.modulith.ApplicationModule(
|
||||
displayName = "Workspace",
|
||||
allowedDependencies = {"analysis::events", "common", "analysis :: entities"})
|
||||
package ai.decompile.workspace;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package ai.decompile.workspace.service;
|
||||
|
||||
import ai.decompile.analysis.event.StaticAnalysisRequestedEvent;
|
||||
import ai.decompile.common.exception.NotFoundException;
|
||||
import ai.decompile.workspace.event.BinaryUploadedEvent;
|
||||
import ai.decompile.workspace.model.dto.BinaryResponse;
|
||||
@ -43,7 +44,7 @@ public class BinaryService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public BinaryResponse uploadBinary(UUID projectId, MultipartFile file) {
|
||||
public BinaryResponse uploadBinary(UUID projectId, MultipartFile file, String engine) {
|
||||
var project = projectService.getProject(projectId);
|
||||
|
||||
String storedPath;
|
||||
@ -68,9 +69,29 @@ public class BinaryService {
|
||||
binary.getProject().getId(),
|
||||
binary.getProject().getWorkspace().getId()));
|
||||
|
||||
if (engine != null && !engine.isBlank()) {
|
||||
eventPublisher.publishEvent(
|
||||
new StaticAnalysisRequestedEvent(
|
||||
binary.getId(),
|
||||
binary.getProject().getId(),
|
||||
binary.getProject().getWorkspace().getId(),
|
||||
engine.strip().toUpperCase()));
|
||||
}
|
||||
|
||||
return BinaryResponse.from(binary);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void requestStaticAnalysis(UUID binaryId, String engine) {
|
||||
var binary = getBinary(binaryId);
|
||||
eventPublisher.publishEvent(
|
||||
new StaticAnalysisRequestedEvent(
|
||||
binary.getId(),
|
||||
binary.getProject().getId(),
|
||||
binary.getProject().getWorkspace().getId(),
|
||||
engine.strip().toUpperCase()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void updateAnalysisInfo(
|
||||
UUID binaryId, String format, String architecture, String compiler) {
|
||||
@ -104,11 +125,19 @@ public class BinaryService {
|
||||
}
|
||||
}
|
||||
|
||||
public String getBinaryFilePath(UUID binaryId) {
|
||||
return getBinary(binaryId).getFilePath();
|
||||
@Transactional
|
||||
public void markStaticAnalysisDone(UUID binaryId) {
|
||||
var binary = getBinary(binaryId);
|
||||
binary.setStaticAnalysisDone(true);
|
||||
binaryRepository.save(binary);
|
||||
}
|
||||
|
||||
private Binary getBinary(UUID binaryId) {
|
||||
public String getBinaryFilePath(UUID binaryId) {
|
||||
var relativePath = getBinary(binaryId).getFilePath();
|
||||
return fileStorage.resolvePath(relativePath);
|
||||
}
|
||||
|
||||
public Binary getBinary(UUID binaryId) {
|
||||
return binaryRepository
|
||||
.findById(binaryId)
|
||||
.orElseThrow(() -> new NotFoundException("Binary with id '" + binaryId + "' not found"));
|
||||
|
||||
@ -9,4 +9,6 @@ public interface FileStorage {
|
||||
void delete(String path) throws IOException;
|
||||
|
||||
InputStream read(String path) throws IOException;
|
||||
|
||||
String resolvePath(String path);
|
||||
}
|
||||
|
||||
@ -41,4 +41,9 @@ public class LocalFileStorage implements FileStorage {
|
||||
var filePath = baseDir.resolve(path);
|
||||
return Files.newInputStream(filePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolvePath(String path) {
|
||||
return baseDir.resolve(path).toAbsolutePath().normalize().toString();
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,6 +36,13 @@ app:
|
||||
image: decompile-ai/diec:latest
|
||||
timeout-seconds: 60
|
||||
docker-host: unix:///var/run/docker.sock
|
||||
engines:
|
||||
ida5:
|
||||
image: decompile-ai/ida5:latest
|
||||
timeout-seconds: 300
|
||||
ghidra:
|
||||
image: decompile-ai/ghidra:latest
|
||||
timeout-seconds: 600
|
||||
|
||||
logging:
|
||||
level:
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
CREATE TABLE static_analysis
|
||||
(
|
||||
id UUID PRIMARY KEY,
|
||||
binary_id UUID NOT NULL REFERENCES binaries (id) ON DELETE CASCADE,
|
||||
job_id UUID REFERENCES jobs (id) ON DELETE SET NULL,
|
||||
engine VARCHAR(20) NOT NULL,
|
||||
status VARCHAR(50) NOT NULL,
|
||||
function_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_static_analysis_binary_id ON static_analysis (binary_id);
|
||||
|
||||
CREATE TABLE static_function
|
||||
(
|
||||
id UUID PRIMARY KEY,
|
||||
analysis_id UUID NOT NULL REFERENCES static_analysis (id) ON DELETE CASCADE,
|
||||
name VARCHAR(500) NOT NULL,
|
||||
address VARCHAR(20) NOT NULL,
|
||||
assembly TEXT,
|
||||
decompiled_code TEXT,
|
||||
signature VARCHAR(1000),
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_static_function_analysis_id ON static_function (analysis_id);
|
||||
CREATE INDEX idx_static_function_name ON static_function (name);
|
||||
|
||||
CREATE TABLE static_xref
|
||||
(
|
||||
id UUID PRIMARY KEY,
|
||||
caller_id UUID NOT NULL REFERENCES static_function (id) ON DELETE CASCADE,
|
||||
callee_id UUID NOT NULL REFERENCES static_function (id) ON DELETE CASCADE,
|
||||
type VARCHAR(20) NOT NULL DEFAULT 'CALL',
|
||||
address VARCHAR(20)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_static_xref_caller_id ON static_xref (caller_id);
|
||||
CREATE INDEX idx_static_xref_callee_id ON static_xref (callee_id);
|
||||
|
||||
ALTER TABLE binaries
|
||||
ADD COLUMN static_analysis_done BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@ -0,0 +1,9 @@
|
||||
CREATE TABLE static_label
|
||||
(
|
||||
id UUID PRIMARY KEY,
|
||||
function_id UUID NOT NULL REFERENCES static_function (id) ON DELETE CASCADE,
|
||||
name VARCHAR(500) NOT NULL,
|
||||
address VARCHAR(20) NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_static_label_function_id ON static_label (function_id);
|
||||
@ -18,6 +18,7 @@ import java.util.UUID;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
@ -31,6 +32,8 @@ class BinaryControllerTest {
|
||||
|
||||
@MockitoBean private BinaryService binaryService;
|
||||
|
||||
@MockitoBean private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
@Test
|
||||
void getBinariesShouldReturn200WithList() throws Exception {
|
||||
var projectId = UUID.randomUUID();
|
||||
@ -45,6 +48,7 @@ class BinaryControllerTest {
|
||||
null,
|
||||
null,
|
||||
"abc_test.exe",
|
||||
false,
|
||||
Instant.now());
|
||||
when(binaryService.getBinaries(eq(projectId), isNull())).thenReturn(List.of(response));
|
||||
|
||||
@ -82,6 +86,7 @@ class BinaryControllerTest {
|
||||
null,
|
||||
null,
|
||||
"abc_filtered.exe",
|
||||
false,
|
||||
Instant.now());
|
||||
when(binaryService.getBinaries(eq(projectId), eq("filter"))).thenReturn(List.of(response));
|
||||
|
||||
@ -111,8 +116,17 @@ class BinaryControllerTest {
|
||||
new MockMultipartFile("file", "test.exe", "application/octet-stream", new byte[] {1, 2, 3});
|
||||
var response =
|
||||
new BinaryResponse(
|
||||
binaryId, projectId, "test.exe", 3L, null, null, null, "abc_test.exe", Instant.now());
|
||||
when(binaryService.uploadBinary(eq(projectId), any())).thenReturn(response);
|
||||
binaryId,
|
||||
projectId,
|
||||
"test.exe",
|
||||
3L,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"abc_test.exe",
|
||||
false,
|
||||
Instant.now());
|
||||
when(binaryService.uploadBinary(eq(projectId), any(), isNull())).thenReturn(response);
|
||||
|
||||
mockMvc
|
||||
.perform(multipart("/projects/{projectId}/binaries", projectId).file(file))
|
||||
|
||||
@ -133,7 +133,7 @@ class BinaryServiceTest {
|
||||
when(fileStorage.store(any(), eq("test.exe"))).thenReturn("stored_test.exe");
|
||||
when(binaryRepository.save(any(Binary.class))).thenReturn(binary);
|
||||
|
||||
BinaryResponse result = binaryService.uploadBinary(projectId, file);
|
||||
BinaryResponse result = binaryService.uploadBinary(projectId, file, null);
|
||||
|
||||
assertEquals(binaryId, result.id());
|
||||
assertEquals("test.bin", result.filename());
|
||||
@ -149,7 +149,7 @@ class BinaryServiceTest {
|
||||
when(projectService.getProject(projectId)).thenReturn(project);
|
||||
when(fileStorage.store(any(), eq("test.exe"))).thenThrow(new IOException("disk full"));
|
||||
|
||||
assertThrows(RuntimeException.class, () -> binaryService.uploadBinary(projectId, file));
|
||||
assertThrows(RuntimeException.class, () -> binaryService.uploadBinary(projectId, file, null));
|
||||
verify(binaryRepository, never()).save(any());
|
||||
verify(eventPublisher, never()).publishEvent(any());
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user