docs: add AI module documentation and AGENTS.md
Comprehensive documentation for the AI module covering architecture, API design, data model, RAG pipeline, configuration, integration points, and frontend integration guide. Includes IDA5 extraction points reference and AGENTS.md with project conventions and coding standards.
This commit is contained in:
parent
198d9f333b
commit
90ee74b3b9
189
AGENTS.md
Normal file
189
AGENTS.md
Normal file
@ -0,0 +1,189 @@
|
||||
# AGENTS.md — decompile-ai
|
||||
|
||||
## Project Overview
|
||||
|
||||
**decompile-ai** is a backend service for binary analysis automation. It accepts uploaded binaries, runs them through multiple analysis engines (IDA Pro, Ghidra, DiE) inside Docker containers, and provides AI-powered chat over the decompiled code via Ollama + RAG (pgvector embeddings).
|
||||
|
||||
- **Java 25**, **Spring Boot 4.0.6**, **Maven** wrapper (`./mvnw`)
|
||||
- **PostgreSQL** + **Flyway** migrations; **H2** in-memory for tests
|
||||
- **RabbitMQ** for async job dispatch
|
||||
- **Spring Modulith** for module boundaries, event publication, and architecture verification
|
||||
- **Spring AI** (Ollama) for chat and embedding generation
|
||||
- **Spotless** (Google Java Format) for code formatting
|
||||
|
||||
## Build & Run Commands
|
||||
|
||||
```bash
|
||||
# Compile
|
||||
./mvnw compile
|
||||
|
||||
# Run tests
|
||||
./mvnw test
|
||||
|
||||
# Run a specific test class
|
||||
./mvnw test -Dtest=ChatServiceTest
|
||||
|
||||
# Package
|
||||
./mvnw package -DskipTests
|
||||
|
||||
# Run the app locally (needs PostgreSQL + RabbitMQ)
|
||||
./mvnw spring-boot:run
|
||||
|
||||
# Format code
|
||||
./mvnw spotless:apply
|
||||
|
||||
# Check formatting (CI-style)
|
||||
./mvnw spotless:check
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/main/java/ai/decompile/
|
||||
├── DecompileAiApplication.java # Spring Boot entry point
|
||||
├── WebConfig.java # CORS configuration
|
||||
├── workspace/ # Workspace, Project, Binary CRUD
|
||||
│ ├── controller/
|
||||
│ ├── event/ # Domain events (BinaryUploadedEvent)
|
||||
│ ├── model/{dto,entity,repository,specification}/
|
||||
│ └── service/
|
||||
├── analysis/ # Static analysis results (functions, xrefs, labels)
|
||||
│ ├── controller/
|
||||
│ ├── event/ # StaticAnalysisRequestedEvent, StaticAnalysisCompletedEvent
|
||||
│ ├── model/{dto,entity,repository}/
|
||||
│ └── service/
|
||||
├── engine/ # Analysis engines: IDA v5, Ghidra (Docker-managed)
|
||||
│ ├── config/
|
||||
│ ├── model/
|
||||
│ └── service/
|
||||
├── die/ # DiE (Detect It Easy) file type detection (Docker)
|
||||
├── docker/ # Docker container management via docker-java
|
||||
├── job/ # Async job queue: create, dispatch, execute, track status
|
||||
│ ├── controller/
|
||||
│ ├── messaging/ # RabbitMQ publisher + listener
|
||||
│ ├── model/{dto,entity,enums,repository,specification}/
|
||||
│ └── service/handler/ # JobType handlers (AnalyzeFile, StaticAnalysis, Embedding)
|
||||
├── ai/ # AI chat + RAG embeddings (Spring AI + Ollama)
|
||||
│ ├── config/
|
||||
│ ├── controller/
|
||||
│ ├── model/{dto,entity,jdbc,repository}/
|
||||
│ └── service/
|
||||
└── common/ # Shared config and exceptions (NotFoundException, ConflictException)
|
||||
└── exception/
|
||||
```
|
||||
|
||||
## Coding Conventions
|
||||
|
||||
### Null Checking
|
||||
Always use `java.util.Objects.isNull()` and `java.util.Objects.nonNull()` — **never** use raw `== null` or `!= null`.
|
||||
|
||||
```java
|
||||
// Correct
|
||||
if (Objects.isNull(value)) { ... }
|
||||
if (Objects.nonNull(value)) { ... }
|
||||
|
||||
// Wrong
|
||||
if (value == null) { ... }
|
||||
if (value != null) { ... }
|
||||
```
|
||||
|
||||
### If Statements
|
||||
Always use braces, even for single-line bodies. Always include a blank line after the closing `}` of an `if` block (before the next statement).
|
||||
|
||||
```java
|
||||
// Correct
|
||||
if (condition) {
|
||||
doSomething();
|
||||
}
|
||||
|
||||
nextThing();
|
||||
|
||||
// Wrong
|
||||
if (condition) doSomething();
|
||||
|
||||
// Wrong (no blank line after if)
|
||||
if (condition) {
|
||||
doSomething();
|
||||
}
|
||||
nextThing();
|
||||
```
|
||||
|
||||
### Classes and Records
|
||||
No blank line between the class/record opening brace and the first member.
|
||||
|
||||
```java
|
||||
// Correct
|
||||
public class Job {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
// Correct
|
||||
public record ChatRequest(@NotBlank String content) {}
|
||||
|
||||
// Wrong (blank line after brace)
|
||||
public class Job {
|
||||
|
||||
@Id
|
||||
private UUID id;
|
||||
```
|
||||
|
||||
### Type Inference (var)
|
||||
Use `var` for local variables whenever the type is obvious from the right-hand side.
|
||||
|
||||
```java
|
||||
// Correct
|
||||
var session = chatService.createSession(binaryId, projectId, model);
|
||||
var messages = new ArrayList<Message>();
|
||||
var binary = binaryService.getBinary(binaryId);
|
||||
|
||||
// Acceptable (type not obvious from RHS, or lambda target typing needed)
|
||||
StreamingChatModel streamingModel = resolveStreamingChatModel(session.getChatModel());
|
||||
```
|
||||
|
||||
### Dependency Injection
|
||||
Constructor injection via Lombok `@RequiredArgsConstructor`. Declare dependencies as `private final`.
|
||||
|
||||
```java
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MyService {
|
||||
private final DependencyA depA;
|
||||
private final DependencyB depB;
|
||||
}
|
||||
```
|
||||
|
||||
### Transactions
|
||||
- `@Transactional` on write operations
|
||||
- `@Transactional(readOnly = true)` on read-only queries
|
||||
- `@Transactional(propagation = Propagation.REQUIRES_NEW)` for independent sub-transactions
|
||||
|
||||
### Testing
|
||||
- **JUnit 5** + **Mockito** (unit tests)
|
||||
- **MockMvc** + H2 (integration tests)
|
||||
- Unit tests: `@ExtendWith(MockitoExtension.class)`, `@Mock` / `@InjectMocks`
|
||||
- Integration tests: `@SpringBootTest`, `@ActiveProfiles("test")`
|
||||
- Test class naming: `*Test.java` (unit), `*IntegrationTest.java` (integration)
|
||||
- Static imports for assertions: `assertEquals(...)`, `assertThrows(...)`, `assertTrue(...)`
|
||||
|
||||
### Logging
|
||||
Lombok `@Log4j2` on all non-trivial classes. Use parameterized messages:
|
||||
|
||||
```java
|
||||
log.info("Processing binary={} with engine={}", binaryId, engineName);
|
||||
log.error("Failed to stream chat for session {}", sessionId, error);
|
||||
```
|
||||
|
||||
### Domain Events (Spring Modulith)
|
||||
Event listeners use `@ApplicationModuleListener`. Events are published via `ApplicationEventPublisher.publishEvent()`.
|
||||
|
||||
### Flyway Migrations
|
||||
- Files in `src/main/resources/db/migration/`, naming: `V001__description.sql`
|
||||
- DDL changes go through Flyway; Hibernate `ddl-auto=validate` only
|
||||
|
||||
## Design Principles
|
||||
|
||||
- **DRY** — Extract repeated logic. The refactoring of `ChatController.sendMessage()` is the canonical example: duplicated error formatting was extracted into reusable `event()` and `errorEvent()` helpers.
|
||||
- **SOLID** — Each module (workspace, analysis, engine, job, ai) has a single responsibility. Dependencies between modules are explicitly declared in `package-info.java`.
|
||||
- **Clean Code** — Small methods, meaningful names, no comments explaining *what* (the code does that), only comments explaining *why* when non-obvious.
|
||||
- **Module boundaries** — Cross-module communication via domain events, not direct service calls. Use `@NamedInterface` for fine-grained intra-module access control.
|
||||
404
docs/ai/api-design.md
Normal file
404
docs/ai/api-design.md
Normal file
@ -0,0 +1,404 @@
|
||||
# API Design — Módulo AI
|
||||
|
||||
## Endpoints
|
||||
|
||||
Todos os endpoints são prefixados com o contexto da aplicação e seguem as convenções REST do projeto (OpenAPI 3.1 via SpringDoc).
|
||||
|
||||
| Método | Path | Descrição | Tag |
|
||||
|---|---|---|---|
|
||||
| `POST` | `/binaries/{binaryId}/chat/sessions` | Criar nova sessão de chat | Chat |
|
||||
| `GET` | `/binaries/{binaryId}/chat/sessions` | Listar sessões do binário | Chat |
|
||||
| `GET` | `/chat/sessions/{sessionId}` | Detalhes da sessão | Chat |
|
||||
| `DELETE` | `/chat/sessions/{sessionId}` | Deletar sessão | Chat |
|
||||
| `GET` | `/chat/sessions/{sessionId}/messages` | Histórico de mensagens | Chat |
|
||||
| `POST` | `/chat/sessions/{sessionId}/messages` | Enviar mensagem (SSE stream) | Chat |
|
||||
|
||||
---
|
||||
|
||||
## 1. Criar Sessão
|
||||
|
||||
### `POST /binaries/{binaryId}/chat/sessions`
|
||||
|
||||
Cria uma nova sessão de chat vinculada a um binário.
|
||||
|
||||
**Query Parameters**:
|
||||
|
||||
| Parâmetro | Tipo | Obrigatório | Default | Descrição |
|
||||
|---|---|---|---|---|
|
||||
| `model` | String | Não | `app.ai.default-chat-provider` | Modelo LLM: `gemma4:12b`, `gemma4:e4b`, `deepseek-chat` |
|
||||
|
||||
**Request Body**: Vazio
|
||||
|
||||
**Response** (`201 Created`):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"binaryId": "660e8400-e29b-41d4-a716-446655440001",
|
||||
"projectId": "770e8400-e29b-41d4-a716-446655440002",
|
||||
"title": "malware.exe — 2026-06-08T15:30:00Z",
|
||||
"chatModel": "gemma4:12b",
|
||||
"messageCount": 0,
|
||||
"createdAt": "2026-06-08T15:30:00Z",
|
||||
"updatedAt": "2026-06-08T15:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Erros**:
|
||||
|
||||
| Código | Condição |
|
||||
|---|---|
|
||||
| `404` | Binário não encontrado |
|
||||
| `400` | Modelo não suportado (ex: `?model=invalid-model`) |
|
||||
|
||||
**Exemplo curl**:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/binaries/{binaryId}/chat/sessions?model=deepseek-chat"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Listar Sessões
|
||||
|
||||
### `GET /binaries/{binaryId}/chat/sessions`
|
||||
|
||||
Lista todas as sessões de chat de um binário, ordenadas por data de atualização (mais recente primeiro).
|
||||
|
||||
**Response** (`200 OK`):
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "550e8400-...",
|
||||
"binaryId": "660e8400-...",
|
||||
"projectId": "770e8400-...",
|
||||
"title": "Analisando funções de rede",
|
||||
"chatModel": "deepseek-chat",
|
||||
"messageCount": 12,
|
||||
"createdAt": "2026-06-08T15:30:00Z",
|
||||
"updatedAt": "2026-06-08T16:45:00Z"
|
||||
},
|
||||
{
|
||||
"id": "550e8401-...",
|
||||
"binaryId": "660e8400-...",
|
||||
"projectId": "770e8400-...",
|
||||
"title": "Investigando crypto",
|
||||
"chatModel": "gemma4:12b",
|
||||
"messageCount": 5,
|
||||
"createdAt": "2026-06-08T14:00:00Z",
|
||||
"updatedAt": "2026-06-08T14:20:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Exemplo curl**:
|
||||
|
||||
```bash
|
||||
curl "http://localhost:8080/binaries/{binaryId}/chat/sessions"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Detalhes da Sessão
|
||||
|
||||
### `GET /chat/sessions/{sessionId}`
|
||||
|
||||
Retorna os detalhes de uma sessão específica.
|
||||
|
||||
**Response** (`200 OK`):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-...",
|
||||
"binaryId": "660e8400-...",
|
||||
"projectId": "770e8400-...",
|
||||
"title": "Analisando funções de rede",
|
||||
"chatModel": "deepseek-chat",
|
||||
"messageCount": 12,
|
||||
"createdAt": "2026-06-08T15:30:00Z",
|
||||
"updatedAt": "2026-06-08T16:45:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Erros**: `404` se sessão não encontrada.
|
||||
|
||||
---
|
||||
|
||||
## 4. Deletar Sessão
|
||||
|
||||
### `DELETE /chat/sessions/{sessionId}`
|
||||
|
||||
Deleta a sessão e todas as suas mensagens (cascade).
|
||||
|
||||
**Response**: `204 No Content`
|
||||
|
||||
**Exemplo curl**:
|
||||
|
||||
```bash
|
||||
curl -X DELETE "http://localhost:8080/chat/sessions/{sessionId}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Histórico de Mensagens
|
||||
|
||||
### `GET /chat/sessions/{sessionId}/messages`
|
||||
|
||||
Retorna o histórico de mensagens da sessão, ordenado cronologicamente (mais antigo primeiro).
|
||||
|
||||
**Response** (`200 OK`):
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "aa0e8400-...",
|
||||
"sessionId": "550e8400-...",
|
||||
"role": "USER",
|
||||
"content": "Quais funções lidam com rede?",
|
||||
"tokenCount": 12,
|
||||
"createdAt": "2026-06-08T15:30:30Z"
|
||||
},
|
||||
{
|
||||
"id": "bb0e8400-...",
|
||||
"sessionId": "550e8400-...",
|
||||
"role": "ASSISTANT",
|
||||
"content": "O binário importa funções de rede da WS2_32.dll. As funções sub_401200 e sub_401300 utilizam send() e recv()...",
|
||||
"tokenCount": 85,
|
||||
"createdAt": "2026-06-08T15:30:35Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Erros**: `404` se sessão não encontrada.
|
||||
|
||||
---
|
||||
|
||||
## 6. Enviar Mensagem (SSE Streaming)
|
||||
|
||||
### `POST /chat/sessions/{sessionId}/messages`
|
||||
|
||||
Envia uma mensagem do usuário e retorna a resposta da LLM em streaming via Server-Sent Events (SSE).
|
||||
|
||||
**Headers**:
|
||||
|
||||
```
|
||||
Content-Type: application/json
|
||||
Accept: text/event-stream
|
||||
```
|
||||
|
||||
**Request Body**:
|
||||
|
||||
```json
|
||||
{
|
||||
"content": "Quais funções nesse binário lidam com criptografia?"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**: `200 OK` com `Content-Type: text/event-stream`
|
||||
|
||||
### Protocolo SSE
|
||||
|
||||
O stream consiste em uma sequência de eventos `data:`:
|
||||
|
||||
#### Evento: `chunk`
|
||||
|
||||
Fragmento de texto da resposta, enviado assim que gerado pela LLM:
|
||||
|
||||
```
|
||||
data: {"type":"chunk","content":"O binário"}
|
||||
|
||||
data: {"type":"chunk","content":" utiliza "}
|
||||
|
||||
data: {"type":"chunk","content":"as seguintes"}
|
||||
|
||||
data: {"type":"chunk","content":" funções"}
|
||||
|
||||
data: {"type":"chunk","content":" relacionadas"}
|
||||
|
||||
data: {"type":"chunk","content":" a criptografia:"}
|
||||
|
||||
data: {"type":"chunk","content":"\n\n1. **sub_405000**"}
|
||||
|
||||
data: {"type":"chunk","content":" em 0x405000"}
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
#### Evento: `done`
|
||||
|
||||
Indica o fim do stream, com metadados da mensagem:
|
||||
|
||||
```
|
||||
data: {"type":"done","messageId":"cc0e8400-e29b-...","sessionId":"550e8400-...","tokenCount":142}
|
||||
```
|
||||
|
||||
#### Evento: `error`
|
||||
|
||||
Indica um erro durante o processamento:
|
||||
|
||||
```
|
||||
data: {"type":"error","code":"EMBEDDING_FAILED","message":"Failed to generate embedding: Ollama connection refused"}
|
||||
```
|
||||
|
||||
Após um evento `error`, a conexão é fechada. Nenhum evento `done` é enviado.
|
||||
|
||||
### Tipos de Evento SSE
|
||||
|
||||
| `type` | Descrição | Campos |
|
||||
|---|---|---|
|
||||
| `chunk` | Fragmento de texto da resposta | `content: String` |
|
||||
| `done` | Stream concluído com sucesso | `messageId: UUID`, `sessionId: UUID`, `tokenCount: Integer` |
|
||||
| `error` | Erro durante o processamento | `code: String`, `message: String` |
|
||||
|
||||
### Fluxo Interno Durante o SSE
|
||||
|
||||
```
|
||||
1. Salva UserMessage (role=USER) no banco
|
||||
2. Embedda a query do usuário (qwen3-embedding)
|
||||
3. Busca similaridade no pgvector (top-K chunks)
|
||||
4. (Opcional) Expande call graph
|
||||
5. Constrói system prompt com contexto RAG
|
||||
6. Monta lista de mensagens: [SystemPrompt, history, UserMessage]
|
||||
7. Chama StreamingChatClient da LLM
|
||||
8. Para cada token gerado: envia SSE "chunk"
|
||||
9. Ao finalizar: salva AssistantMessage (role=ASSISTANT) no banco
|
||||
10. Envia SSE "done" com messageId + tokenCount
|
||||
11. Se erro em qualquer passo: envia SSE "error" e fecha conexão
|
||||
```
|
||||
|
||||
### Exemplo curl (com streaming)
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/chat/sessions/{sessionId}/messages" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: text/event-stream" \
|
||||
-d '{"content": "Explique a função sub_401000"}'
|
||||
```
|
||||
|
||||
### Implementação no Controller
|
||||
|
||||
```java
|
||||
@PostMapping(value = "/chat/sessions/{sessionId}/messages",
|
||||
produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
@Operation(summary = "Send a message and stream the AI response",
|
||||
tags = {"Chat"})
|
||||
public Flux<ServerSentEvent<String>> sendMessage(
|
||||
@PathVariable UUID sessionId,
|
||||
@Valid @RequestBody ChatRequest request) {
|
||||
|
||||
return chatService.chat(sessionId, request.content())
|
||||
.map(token -> ServerSentEvent.<String>builder()
|
||||
.data("{\"type\":\"chunk\",\"content\":\"" + escapeJson(token) + "\"}")
|
||||
.build())
|
||||
.concatWith(chatService.finalizeMessage(sessionId)
|
||||
.map(msg -> ServerSentEvent.<String>builder()
|
||||
.data("{\"type\":\"done\",\"messageId\":\"" + msg.id() + "\","
|
||||
+ "\"sessionId\":\"" + sessionId + "\","
|
||||
+ "\"tokenCount\":" + msg.tokenCount() + "}")
|
||||
.build()))
|
||||
.onErrorResume(e -> Flux.just(
|
||||
ServerSentEvent.<String>builder()
|
||||
.data("{\"type\":\"error\",\"code\":\"INTERNAL\","
|
||||
+ "\"message\":\"" + escapeJson(e.getMessage()) + "\"}")
|
||||
.build()));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DTOs
|
||||
|
||||
### `ChatRequest`
|
||||
|
||||
```java
|
||||
public record ChatRequest(
|
||||
@NotBlank @Size(min = 1, max = 4000)
|
||||
String content
|
||||
) {}
|
||||
```
|
||||
|
||||
### `ChatResponse` (evento `done`)
|
||||
|
||||
```java
|
||||
public record ChatResponse(
|
||||
UUID messageId,
|
||||
UUID sessionId,
|
||||
int tokenCount
|
||||
) {}
|
||||
```
|
||||
|
||||
### `SessionResponse`
|
||||
|
||||
```java
|
||||
public record SessionResponse(
|
||||
UUID id,
|
||||
UUID binaryId,
|
||||
UUID projectId,
|
||||
String title,
|
||||
String chatModel,
|
||||
int messageCount,
|
||||
Instant createdAt,
|
||||
Instant updatedAt
|
||||
) {}
|
||||
```
|
||||
|
||||
### `MessageResponse`
|
||||
|
||||
```java
|
||||
public record MessageResponse(
|
||||
UUID id,
|
||||
UUID sessionId,
|
||||
String role,
|
||||
String content,
|
||||
Integer tokenCount,
|
||||
Instant createdAt
|
||||
) {}
|
||||
```
|
||||
|
||||
### `StreamChunk` (evento SSE `chunk`)
|
||||
|
||||
```java
|
||||
public record StreamChunk(
|
||||
String type, // "chunk", "done", "error"
|
||||
String content // null para "done" e "error"
|
||||
) {}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tratamento de Erros
|
||||
|
||||
| Código HTTP | `type` SSE | Condição |
|
||||
|---|---|---|
|
||||
| `400` | — | Request body inválido (content vazio, >4000 chars) |
|
||||
| `404` | — | Sessão não encontrada |
|
||||
| `404` | — | Binário não encontrado |
|
||||
| `503` | `error` | Ollama indisponível (embeddings ou chat) |
|
||||
| `503` | `error` | DeepSeek API indisponível |
|
||||
| `500` | `error` | Erro interno (pgvector, RabbitMQ, etc.) |
|
||||
| `402` | `error` | DeepSeek API: saldo insuficiente / quota excedida |
|
||||
|
||||
### Resposta de erro padrão (não-SSE)
|
||||
|
||||
```json
|
||||
{
|
||||
"status": 404,
|
||||
"message": "Chat session with id '550e8400-...' not found",
|
||||
"timestamp": "2026-06-08T15:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## OpenAPI / Swagger
|
||||
|
||||
Todos os endpoints são documentados com `@Operation` e aparecem no Swagger UI (`/swagger-ui.html`).
|
||||
|
||||
**Tag**: `Chat`
|
||||
|
||||
**Schemas**:
|
||||
- `ChatRequest`
|
||||
- `SessionResponse`
|
||||
- `MessageResponse`
|
||||
- `StreamChunk` (não documentado como schema, pois é SSE)
|
||||
281
docs/ai/architecture.md
Normal file
281
docs/ai/architecture.md
Normal file
@ -0,0 +1,281 @@
|
||||
# Arquitetura do Módulo de IA
|
||||
|
||||
## Visão Geral
|
||||
|
||||
O módulo `ai/` implementa um sistema de **Retrieval-Augmented Generation (RAG)** que permite ao usuário interagir com uma LLM através de um chat, fazendo perguntas sobre funções, estruturas, protocolos de rede e outros aspectos de um binário que passou por análise estática.
|
||||
|
||||
O sistema combina **busca semântica via embeddings** (pgvector) com **navegação estruturada via call graph** (xrefs) para fornecer contexto relevante à LLM durante a geração de respostas.
|
||||
|
||||
## Stack Tecnológica
|
||||
|
||||
| Componente | Tecnologia | Papel |
|
||||
|---|---|---|
|
||||
| **Framework** | Spring Boot 4.0.6 + Spring Modulith 2.0.6 | Base da aplicação, injeção de dependências, eventos |
|
||||
| **AI Framework** | Spring AI 1.0.x | Abstração de LLM providers, embeddings, chat streaming |
|
||||
| **Chat Local** | Gemma 4 12B (via Ollama) | LLM para chat executando localmente, 256K contexto |
|
||||
| **Chat Cloud** | DeepSeek (`deepseek-chat`) | LLM alternativa via API cloud (OpenAI-compatible) |
|
||||
| **Embeddings** | Qwen3 Embedding 8B (via Ollama) | Geração de embeddings para busca semântica, dims 32–4096 |
|
||||
| **Vector Store** | pgvector (PostgreSQL 18) | Armazenamento e busca por similaridade de cosseno |
|
||||
| **Vector Index** | HNSW (Hierarchical Navigable Small World) | Índice para busca vetorial rápida |
|
||||
| **Message Broker** | RabbitMQ 4 | Processamento assíncrono de jobs de embedding |
|
||||
| **Arquitetura** | Spring Modulith | Módulo desacoplado com eventos de domínio |
|
||||
|
||||
## Diagrama de Componentes
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ ai.decompile.ai (Módulo AI) │
|
||||
│ │
|
||||
│ ┌──────────────────────┐ ┌──────────────────────┐ │
|
||||
│ │ EmbeddingService │ │ ChatService │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ • embed(text) │ │ • createSession() │ │
|
||||
│ │ • embedBatch() │ │ • chat(sessionId, │ │
|
||||
│ │ • indexBinary(id) │ │ message) │ │
|
||||
│ │ • searchSimilar() │ │ • getHistory() │ │
|
||||
│ │ • deleteByBinary() │ │ • deleteSession() │ │
|
||||
│ └────────┬─────────────┘ └──────────┬───────────┘ │
|
||||
│ │ │ │
|
||||
│ ┌────────┴─────────────────────────────┴──────────────────┐ │
|
||||
│ │ RagService │ │
|
||||
│ │ │ │
|
||||
│ │ • retrieveContext(binaryId, query) → List<Chunk> │ │
|
||||
│ │ • buildSystemPrompt(chunks, binaryMeta) → String │ │
|
||||
│ │ • expandCallGraph(functionId, hops) → List<Function> │ │
|
||||
│ │ • deduplicateAndRank(results) → List<Chunk> │ │
|
||||
│ └────────┬──────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────┴────────────────┬──────────────────────────────┐ │
|
||||
│ │ EmbeddingChunkRepo │ ChatSessionRepo │ │
|
||||
│ │ • findSimilar(vector, │ • findByBinaryId() │ │
|
||||
│ │ binaryId, topK) │ • findById() │ │
|
||||
│ │ • deleteByBinaryId() │ │ │
|
||||
│ │ │ ChatMessageRepo │ │
|
||||
│ │ │ • findBySessionId() │ │
|
||||
│ └──────────────────────────┴──────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ ChatController (REST + SSE) │ │
|
||||
│ │ │ │
|
||||
│ │ POST /binaries/{id}/chat/sessions — criar sessão │ │
|
||||
│ │ GET /binaries/{id}/chat/sessions — listar sessões │ │
|
||||
│ │ GET /chat/sessions/{id}/messages — histórico │ │
|
||||
│ │ POST /chat/sessions/{id}/messages — enviar (SSE) │ │
|
||||
│ │ DELETE /chat/sessions/{id} — deletar sessão │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ Infraestrutura Externa │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Ollama │ │ DeepSeek │ │ PostgreSQL │ │
|
||||
│ │ (localhost) │ │ (Cloud API) │ │ (pgvector) │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ qwen3-embed │ │ deepseek- │ │ embedding_ │ │
|
||||
│ │ gemma4:12b │ │ chat │ │ chunk │ │
|
||||
│ └──────────────┘ └──────────────┘ │ chat_session │ │
|
||||
│ │ chat_message │ │
|
||||
│ ┌──────────────┐ └──────────────┘ │
|
||||
│ │ RabbitMQ │ │
|
||||
│ │ │ │
|
||||
│ │ decompile. │ │
|
||||
│ │ jobs.queue │ │
|
||||
│ └──────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Decisões Arquiteturais
|
||||
|
||||
### 1. Uso do Spring AI como Camada de Abstração
|
||||
|
||||
**Decisão**: Utilizar Spring AI 1.0.x como camada de abstração para providers de LLM e embeddings.
|
||||
|
||||
**Justificativa**:
|
||||
- Fornece interfaces padronizadas (`ChatClient`, `StreamingChatClient`, `EmbeddingModel`)
|
||||
- Suporte nativo a streaming via `Flux<String>` (Reactor)
|
||||
- Auto-configuração para Ollama e OpenAI (DeepSeek é compatível com API da OpenAI)
|
||||
- Integração com ecossistema Spring Boot
|
||||
|
||||
**Trade-off**: Spring AI é relativamente novo e pode ter breaking changes entre versões. Mitigamos encapsulando os chamados diretos atrás dos nossos serviços (`EmbeddingService`, `ChatService`).
|
||||
|
||||
### 2. Embedding Store Próprio (não Spring AI VectorStore)
|
||||
|
||||
**Decisão**: Implementar repositório pgvector próprio (`EmbeddingChunkRepository`) em vez de usar `spring-ai-pgvector-store`.
|
||||
|
||||
**Justificativa**:
|
||||
- Schema altamente específico ao domínio (FK para `binary_id`, `source_id` para `static_function.id`)
|
||||
- Necessidade de campos de metadados customizados em `jsonb`
|
||||
- Semântica de deleção específica (hard delete por `binary_id` na re-indexação)
|
||||
- Suporte a múltiplas estratégias de retrieval: busca semântica (pgvector `<=>`), busca textual (ILIKE fallback), call graph expansion
|
||||
|
||||
### 2.1 Hybrid Retrieval
|
||||
|
||||
**Decisão**: Pipeline de retrieval com três camadas: semântica → textual fallback → call graph.
|
||||
|
||||
**Fluxo**:
|
||||
1. **Semântica**: pgvector cosine similarity (top-K)
|
||||
2. **Fallback**: se < 40% do topK preenchido, busca textual via `LIKE` nos chunks
|
||||
3. **Call Graph**: detecta funções/endereços na query, expande 1-hop via xrefs, inclui assembly inline
|
||||
|
||||
**Justificativa**:
|
||||
- Queries com nomes específicos de função (`sub_f5`, `0x401000`) frequentemente falham na busca semântica (nome curto, sem contexto)
|
||||
- O fallback textual captura menções diretas ao nome da função nos chunks
|
||||
- A expansão por call graph garante que a função requisitada SEMPRE apareça no contexto, mesmo sem embedding
|
||||
|
||||
### 3. Indexação Assíncrona via Job Separado
|
||||
|
||||
**Decisão**: Geração de embeddings como job assíncrono (`GENERATE_EMBEDDINGS`) via RabbitMQ, desacoplado do job de análise estática.
|
||||
|
||||
**Justificativa**:
|
||||
- Análise estática e embedding são operações com perfis de falha diferentes
|
||||
- Permite re-indexar sem re-analisar (ex: trocar modelo de embedding)
|
||||
- Permite retry independente se a API de embedding falhar
|
||||
- Segue o padrão arquitetural já estabelecido (ANALYZE_FILE, STATIC_ANALYSIS)
|
||||
|
||||
### 4. Multi-Provider com Seleção por Sessão
|
||||
|
||||
**Decisão**: Suporte a múltiplos providers de LLM (Ollama/Gemma4 + DeepSeek) com seleção no momento da criação da sessão.
|
||||
|
||||
**Justificativa**:
|
||||
- Usuário pode escolher entre privacidade/local (Ollama) ou qualidade/cloud (DeepSeek)
|
||||
- Providers são "pluggable" — adicionar novo provider requer apenas implementar a interface e registrar
|
||||
- A seleção é persistida na `chat_session.chat_model` e usada em todas as mensagens daquela sessão
|
||||
|
||||
### 5. Streaming via Server-Sent Events (SSE)
|
||||
|
||||
**Decisão**: Respostas do chat utilizam SSE para streaming de tokens.
|
||||
|
||||
**Justificativa**:
|
||||
- UX muito melhor que esperar resposta completa
|
||||
- Nativo do Spring WebMVC / Reactor (`Flux<ServerSentEvent<String>>`)
|
||||
- Compatível com `StreamingChatClient` do Spring AI
|
||||
- Fácil de consumir no frontend (`EventSource` API)
|
||||
|
||||
### 6. Chunking por Função (não por tamanho fixo)
|
||||
|
||||
**Decisão**: Cada função estática vira um chunk de embedding. Funções muito grandes são truncadas em `chunk-max-chars` (default 3000).
|
||||
|
||||
**Justificativa**:
|
||||
- Alinha com a unidade semântica natural da análise de binários (a função)
|
||||
- Permite que metadados de call graph (callers/callees) enriqueçam cada chunk
|
||||
- Facilita a expansão via navegação de xrefs (saber exatamente qual função foi recuperada)
|
||||
- Simplifica o tracking (`source_type=STATIC_FUNCTION`, `source_id=function.id`)
|
||||
|
||||
## Módulos Spring Modulith
|
||||
|
||||
### Dependências do módulo `ai/`
|
||||
|
||||
```
|
||||
ai/
|
||||
├── allowedDependencies:
|
||||
│ ├── workspace::entities (Binary, Project para relações FK)
|
||||
│ ├── analysis::entities (StaticFunction, StaticAnalysis, etc.)
|
||||
│ ├── analysis::services (AnalysisService para buscar funções/xrefs)
|
||||
│ └── common (exceções, configurações compartilhadas)
|
||||
```
|
||||
|
||||
### Dependências do módulo `job/` (atualizado)
|
||||
|
||||
```
|
||||
job/
|
||||
├── allowedDependencies (novos):
|
||||
│ ├── ...
|
||||
│ ├── ai::services (EmbeddingService para o handler de embeddings)
|
||||
│ ├── ai::dto (tipos de dados do módulo AI)
|
||||
│ └── ai::events (eventos publicados pelo módulo AI)
|
||||
```
|
||||
|
||||
### Dependências do módulo `analysis/` (atualizado)
|
||||
|
||||
```
|
||||
analysis/
|
||||
├── allowedDependencies (novos):
|
||||
│ ├── ...
|
||||
│ └── ai::events (para publicar StaticAnalysisCompletedEvent)
|
||||
└── NOTA: o evento será definido no analysis/event para evitar dependência circular
|
||||
```
|
||||
|
||||
## Fluxo de Eventos Inter-Módulos
|
||||
|
||||
```
|
||||
workspace analysis job ai
|
||||
│ │ │ │
|
||||
│ BinaryUp- │ │ │
|
||||
│ loadedEvent───┤ │ │
|
||||
│ │ │ │
|
||||
│ │ StaticAnalysisRequestedEvent │
|
||||
│ ├───────────────►│ │
|
||||
│ │ │ STATIC_ │
|
||||
│ │ │ ANALYSIS │
|
||||
│ │ │ (handler) │
|
||||
│ │ │ │
|
||||
│ │◄───────────────┤ saveResult() │
|
||||
│ │ │ │
|
||||
│ │ StaticAnalysisCompletedEvent │
|
||||
│ ├────────────────────────────────►│
|
||||
│ │ │ │
|
||||
│ │ │ GENERATE_ │
|
||||
│ │ │ EMBEDDINGS │
|
||||
│ │ │ (handler)────►│
|
||||
│ │ │ │
|
||||
│ │ │◄───────────────│
|
||||
│ │ │ indexBinary() │
|
||||
```
|
||||
|
||||
> **Nota**: O `StaticAnalysisCompletedEvent` será definido em `analysis.event` e o subscriber residirá em `job.service` (padrão consistente com `StaticAnalysisRequestedEvent` + `StaticAnalysisEventSubscriber`).
|
||||
|
||||
## Configuração Multi-Provider
|
||||
|
||||
O módulo suporta dois providers de chat e um provider de embeddings, todos abstraídos pelo Spring AI:
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ ChatClientProvider │ (interface própria)
|
||||
│ │
|
||||
│ getClient(model)─────┤
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
│ │
|
||||
┌──────────┴──────────┐ ┌──────────────┴──────────┐
|
||||
│ DeepSeekChatClient │ │ OllamaChatClient │
|
||||
│ (OpenAiChatModel) │ │ (OllamaChatModel) │
|
||||
│ │ │ │
|
||||
│ base-url: │ │ base-url: │
|
||||
│ api.deepseek.com │ │ localhost:11434 │
|
||||
│ model: deepseek-chat│ │ model: gemma4:12b │
|
||||
└─────────────────────┘ └─────────────────────────┘
|
||||
|
||||
┌─────────────────────┐
|
||||
│ EmbeddingModel │ (Spring AI interface)
|
||||
│ │
|
||||
│ embed(docs) │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌──────────┴──────────┐
|
||||
│ OllamaEmbeddingModel │
|
||||
│ │
|
||||
│ base-url: │
|
||||
│ localhost:11434 │
|
||||
│ model: qwen3-embedding│
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
## Considerações de Performance
|
||||
|
||||
| Aspecto | Estratégia |
|
||||
|---|---|
|
||||
| **Embedding em batch** | Funções enviadas em lotes de 20 (`app.ai.embedding.batch-size`) |
|
||||
| **Busca vetorial** | Índice HNSW com `vector_cosine_ops` — sub-100ms para milhares de vetores |
|
||||
| **Contexto da LLM** | Truncagem de chunks + histórico para caber em `max-context-tokens` (8000 default) |
|
||||
| **Streaming** | Tokens fluem via SSE assim que gerados, sem buffer completo |
|
||||
| **Conexões Ollama** | Pool de conexões HTTP gerenciado pelo Spring AI |
|
||||
| **Virtual Threads** | Habilitados (`spring.threads.virtual.enabled: true`) — I/O de rede não bloqueia |
|
||||
|
||||
## Segurança
|
||||
|
||||
- API keys (DeepSeek) armazenadas exclusivamente em `.env`, nunca no repositório
|
||||
- `.env` listado no `.gitignore`
|
||||
- Nenhuma chave ou segredo em plaintext no `application.yml`
|
||||
- Variáveis de ambiente referenciadas como `${DEEPSEEK_API_KEY}` com fallback vazio
|
||||
438
docs/ai/configuration.md
Normal file
438
docs/ai/configuration.md
Normal file
@ -0,0 +1,438 @@
|
||||
# Guia de Configuração — Módulo AI
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
| Componente | Como obter | Status |
|
||||
|---|---|---|
|
||||
| **Ollama** (host) | `curl -fsSL https://ollama.com/install.sh \| sh` | Local, necessário |
|
||||
| **qwen3-embedding** | `ollama pull qwen3-embedding:latest` | Modelo de embedding |
|
||||
| **gemma4:12b** | `ollama pull gemma4:12b` | LLM para chat local |
|
||||
| **DeepSeek API key** | [platform.deepseek.com](https://platform.deepseek.com) | LLM cloud (opcional) |
|
||||
| **PostgreSQL + pgvector** | `docker compose up -d postgres` | Já configurado |
|
||||
| **RabbitMQ** | `docker compose up -d rabbit-mq` | Já configurado |
|
||||
|
||||
## 1. Setup do Ollama (Host)
|
||||
|
||||
### Instalação
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
|
||||
# Verificar se está rodando
|
||||
ollama --version
|
||||
curl http://localhost:11434/api/tags
|
||||
```
|
||||
|
||||
### Download dos Modelos
|
||||
|
||||
```bash
|
||||
# Embedding (4.7 GB)
|
||||
ollama pull qwen3-embedding:latest
|
||||
|
||||
# Chat local (7.6 GB)
|
||||
ollama pull gemma4:12b
|
||||
|
||||
# Alternativa menor para máquinas com pouca RAM (9.6 GB, MoE)
|
||||
# ollama pull gemma4:e4b
|
||||
|
||||
# Verificar modelos instalados
|
||||
ollama list
|
||||
```
|
||||
|
||||
**Hardware recomendado**:
|
||||
|
||||
| Modelo | RAM mínima | RAM recomendada | GPU VRAM (opcional) |
|
||||
|---|---|---|---|
|
||||
| `qwen3-embedding:latest` (8B) | 8 GB | 16 GB | 6 GB |
|
||||
| `gemma4:12b` | 10 GB | 16 GB | 8 GB |
|
||||
| `gemma4:e4b` | 6 GB | 12 GB | 6 GB |
|
||||
|
||||
### Configuração do Ollama
|
||||
|
||||
Por padrão, o Ollama escuta em `localhost:11434`. Se precisar acessar de outro host:
|
||||
|
||||
```bash
|
||||
# Editar /etc/systemd/system/ollama.service ou usar env var
|
||||
export OLLAMA_HOST=0.0.0.0:11434
|
||||
```
|
||||
|
||||
Para otimizar performance com GPU (se disponível):
|
||||
|
||||
```bash
|
||||
# Verificar se GPU está sendo usada
|
||||
ollama run gemma4:12b --verbose
|
||||
# Saída: "total duration: ..." — procure por "eval rate: XX tokens/s"
|
||||
```
|
||||
|
||||
## 2. Arquivo `.env`
|
||||
|
||||
Criar na raiz do projeto (`backend/.env`):
|
||||
|
||||
```bash
|
||||
# ============================================================
|
||||
# Decompile-AI — Environment Variables
|
||||
# ============================================================
|
||||
|
||||
# --- LLM Providers ---
|
||||
|
||||
# DeepSeek API Key (obter em https://platform.deepseek.com)
|
||||
DEEPSEEK_API_KEY=sk-your-api-key-here
|
||||
|
||||
# --- Override de modelos (opcional) ---
|
||||
|
||||
# Provedor padrão de chat: ollama | deepseek
|
||||
AI_DEFAULT_CHAT_PROVIDER=ollama
|
||||
|
||||
# Modelo de chat Ollama (padrão: gemma4:12b)
|
||||
# AI_CHAT_MODEL_OLLAMA=gemma4:12b
|
||||
|
||||
# Modelo de chat DeepSeek (padrão: deepseek-chat)
|
||||
# AI_CHAT_MODEL_DEEPSEEK=deepseek-chat
|
||||
|
||||
# Modelo de embedding (padrão: qwen3-embedding:latest)
|
||||
# AI_EMBEDDING_MODEL=qwen3-embedding:latest
|
||||
|
||||
# Dimensão do embedding (padrão: 1024, max 4096 para qwen3)
|
||||
# AI_EMBEDDING_DIMENSION=1024
|
||||
|
||||
# --- Database ---
|
||||
|
||||
# Sobrescrever credenciais do Postgres se necessário
|
||||
# SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/decompile_ai
|
||||
# SPRING_DATASOURCE_USERNAME=decompile_ai
|
||||
# SPRING_DATASOURCE_PASSWORD=decompile_ai
|
||||
```
|
||||
|
||||
**Importante**: Adicione `.env` ao `.gitignore`:
|
||||
|
||||
```bash
|
||||
echo ".env" >> .gitignore
|
||||
```
|
||||
|
||||
## 3. Carregamento do `.env`
|
||||
|
||||
O Spring Boot precisa saber como carregar o `.env`. Opções:
|
||||
|
||||
### Opção A: `spring.config.import` (nativo Spring Boot)
|
||||
|
||||
Adicionar ao `application.yml`:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
config:
|
||||
import: optional:file:.env[.properties]
|
||||
```
|
||||
|
||||
Isso carrega o `.env` como um arquivo de propriedades. **Limitação**: não suporta substituição de variáveis inline como `${VAR}` dentro de outras variáveis no `.env`, mas funciona bem para definir valores.
|
||||
|
||||
### Opção B: Plugin `dotenv` (recomendado para desenvolvimento)
|
||||
|
||||
Adicionar dependência ao `pom.xml`:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>me.paulschwarz</groupId>
|
||||
<artifactId>spring-dotenv</artifactId>
|
||||
<version>4.0.0</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Suporta interpolação de variáveis e convenção `.env` padrão.
|
||||
|
||||
### Opção C: Export manual (desenvolvimento)
|
||||
|
||||
```bash
|
||||
set -a && source .env && set +a
|
||||
./mvnw spring-boot:run
|
||||
```
|
||||
|
||||
## 4. `application.yml` — Configuração do Módulo AI
|
||||
|
||||
### Blocos a adicionar ao arquivo existente
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
# ... configurações existentes (datasource, jpa, flyway, rabbitmq) ...
|
||||
|
||||
ai:
|
||||
ollama:
|
||||
base-url: http://localhost:11434
|
||||
embedding:
|
||||
options:
|
||||
model: ${AI_EMBEDDING_MODEL:qwen3-embedding:latest}
|
||||
chat:
|
||||
options:
|
||||
model: ${AI_CHAT_MODEL_OLLAMA:gemma4:12b}
|
||||
temperature: 0.3 # respostas determinísticas para análise técnica
|
||||
top-p: 0.95
|
||||
top-k: 64
|
||||
openai:
|
||||
base-url: https://api.deepseek.com
|
||||
api-key: ${DEEPSEEK_API_KEY}
|
||||
chat:
|
||||
options:
|
||||
model: ${AI_CHAT_MODEL_DEEPSEEK:deepseek-chat}
|
||||
temperature: 0.3
|
||||
max-tokens: 4096
|
||||
|
||||
app:
|
||||
# ... configurações existentes (storage, rabbitmq, docker, die, engines) ...
|
||||
|
||||
ai:
|
||||
# Provedor padrão de chat quando a sessão não especifica modelo
|
||||
default-chat-provider: ${AI_DEFAULT_CHAT_PROVIDER:ollama}
|
||||
|
||||
# --- Configurações de Embedding ---
|
||||
embedding:
|
||||
# Dimensão do vetor (qwen3-embedding suporta 32-4096)
|
||||
# DEVE corresponder à coluna vector(N) na migration V008
|
||||
dimension: ${AI_EMBEDDING_DIMENSION:1024}
|
||||
# Tamanho máximo do chunk em caracteres
|
||||
chunk-max-chars: 3000
|
||||
# Quantas funções processar por batch na API de embedding
|
||||
batch-size: 20
|
||||
|
||||
# --- Configurações de RAG ---
|
||||
rag:
|
||||
# Quantos chunks retornar na busca semântica
|
||||
top-k: 10
|
||||
# Similaridade mínima para incluir um chunk (0.0 a 1.0)
|
||||
similarity-threshold: 0.6
|
||||
# Máximo de tokens de contexto enviados à LLM
|
||||
max-context-tokens: 8000
|
||||
|
||||
# --- Configurações de Chat ---
|
||||
chat:
|
||||
# Quantas mensagens de histórico incluir no prompt
|
||||
max-history-messages: 20
|
||||
# Temperatura padrão para geração
|
||||
temperature: 0.3
|
||||
```
|
||||
|
||||
### Configuração completa do `application.yml` (após merge)
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
threads:
|
||||
virtual:
|
||||
enabled: true
|
||||
application:
|
||||
name: decompile-ai
|
||||
config:
|
||||
import: optional:file:.env[.properties]
|
||||
datasource:
|
||||
url: jdbc:postgresql://localhost:5432/decompile_ai
|
||||
username: decompile_ai
|
||||
password: decompile_ai
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
flyway:
|
||||
enabled: true
|
||||
rabbitmq:
|
||||
host: localhost
|
||||
port: 5672
|
||||
username: guest
|
||||
password: guest
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 500MB
|
||||
max-request-size: 500MB
|
||||
ai:
|
||||
ollama:
|
||||
base-url: http://localhost:11434
|
||||
embedding:
|
||||
options:
|
||||
model: ${AI_EMBEDDING_MODEL:qwen3-embedding:latest}
|
||||
chat:
|
||||
options:
|
||||
model: ${AI_CHAT_MODEL_OLLAMA:gemma4:12b}
|
||||
temperature: 0.3
|
||||
top-p: 0.95
|
||||
top-k: 64
|
||||
openai:
|
||||
base-url: https://api.deepseek.com
|
||||
api-key: ${DEEPSEEK_API_KEY}
|
||||
chat:
|
||||
options:
|
||||
model: ${AI_CHAT_MODEL_DEEPSEEK:deepseek-chat}
|
||||
temperature: 0.3
|
||||
max-tokens: 4096
|
||||
|
||||
app:
|
||||
storage:
|
||||
upload-dir: ./uploads/binaries
|
||||
rabbitmq:
|
||||
exchange: decompile.jobs
|
||||
queue: decompile.jobs.queue
|
||||
routing-key: decompile.jobs.created
|
||||
docker:
|
||||
enabled: true
|
||||
die:
|
||||
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
|
||||
ai:
|
||||
default-chat-provider: ${AI_DEFAULT_CHAT_PROVIDER:ollama}
|
||||
embedding:
|
||||
dimension: ${AI_EMBEDDING_DIMENSION:1024}
|
||||
chunk-max-chars: 3000
|
||||
batch-size: 20
|
||||
rag:
|
||||
top-k: 10
|
||||
similarity-threshold: 0.6
|
||||
max-context-tokens: 8000
|
||||
chat:
|
||||
max-history-messages: 20
|
||||
temperature: 0.3
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.springframework.modulith: DEBUG
|
||||
ai.decompile.ai: DEBUG # Logs do módulo AI
|
||||
org.springframework.ai: INFO # Logs do Spring AI
|
||||
```
|
||||
|
||||
## 5. `docker-compose.yml` (sem alterações)
|
||||
|
||||
O `docker-compose.yml` atual já atende todos os requisitos:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
container_name: decompile-ai-postgres
|
||||
image: pgvector/pgvector:pg18 # PostgreSQL 18 + pgvector nativo
|
||||
environment:
|
||||
- POSTGRES_USER=decompile_ai
|
||||
- POSTGRES_PASSWORD=decompile_ai
|
||||
- POSTGRES_DB=decompile_ai
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres:/var/lib/postgresql
|
||||
networks:
|
||||
- decompile-ai-network
|
||||
|
||||
rabbit-mq:
|
||||
container_name: decompile-ai-rabbitmq
|
||||
image: rabbitmq:4-management
|
||||
ports:
|
||||
- "5672:5672"
|
||||
- "15672:15672"
|
||||
networks:
|
||||
- decompile-ai-network
|
||||
|
||||
networks:
|
||||
decompile-ai-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
postgres:
|
||||
```
|
||||
|
||||
**Não adicionamos Ollama ao docker-compose** porque ele roda no host.
|
||||
|
||||
## 6. Startup
|
||||
|
||||
### Ordem de inicialização
|
||||
|
||||
```bash
|
||||
# 1. Infraestrutura Docker
|
||||
docker compose up -d postgres rabbit-mq
|
||||
|
||||
# 2. Verificar Ollama no host
|
||||
ollama list
|
||||
# Deve mostrar: qwen3-embedding:latest, gemma4:12b
|
||||
|
||||
# 3. Configurar .env
|
||||
cp .env.example .env
|
||||
# Editar com a DEEPSEEK_API_KEY
|
||||
|
||||
# 4. Iniciar o backend
|
||||
./mvnw spring-boot:run
|
||||
|
||||
# 5. Verificar health
|
||||
curl http://localhost:8080/actuator/health
|
||||
```
|
||||
|
||||
### Verificação de saúde dos componentes AI
|
||||
|
||||
```bash
|
||||
# Ollama
|
||||
curl http://localhost:11434/api/tags | jq '.models[].name'
|
||||
|
||||
# Testar embedding
|
||||
curl http://localhost:11434/api/embed -d '{
|
||||
"model": "qwen3-embedding:latest",
|
||||
"input": "test"
|
||||
}' | jq '.embeddings[0] | length'
|
||||
# Deve retornar 1024 (ou dimensão configurada)
|
||||
|
||||
# Testar chat local
|
||||
curl http://localhost:11434/api/chat -d '{
|
||||
"model": "gemma4:12b",
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
"stream": false
|
||||
}' | jq '.message.content'
|
||||
```
|
||||
|
||||
## 7. Troubleshooting
|
||||
|
||||
### Ollama não responde
|
||||
|
||||
```bash
|
||||
# Verificar se está rodando
|
||||
systemctl status ollama
|
||||
# ou
|
||||
ps aux | grep ollama
|
||||
|
||||
# Reiniciar se necessário
|
||||
systemctl restart ollama
|
||||
```
|
||||
|
||||
### Erro: "model not found"
|
||||
|
||||
```bash
|
||||
# Listar modelos disponíveis
|
||||
ollama list
|
||||
|
||||
# Baixar modelo faltante
|
||||
ollama pull qwen3-embedding:latest
|
||||
ollama pull gemma4:12b
|
||||
```
|
||||
|
||||
### Erro: "DeepSeek API key not configured"
|
||||
|
||||
- Verificar se `.env` existe e contém `DEEPSEEK_API_KEY=sk-...`
|
||||
- Verificar se `spring.config.import: optional:file:.env[.properties]` está no `application.yml`
|
||||
- Alternativa: `export DEEPSEEK_API_KEY=sk-...` no terminal
|
||||
|
||||
### Erro: "relation 'vector_store' does not exist"
|
||||
|
||||
- Verificar se Flyway rodou a migration V008: `SELECT * FROM flyway_schema_history;`
|
||||
- Rodar manualmente se necessário: `./mvnw flyway:migrate`
|
||||
|
||||
### Erro: "column 'embedding' type vector not supported"
|
||||
|
||||
- Verificar se a extensão pgvector está ativada: `SELECT * FROM pg_extension WHERE extname = 'vector';`
|
||||
- Ativar manualmente: `CREATE EXTENSION IF NOT EXISTS vector;`
|
||||
|
||||
### Performance lenta nos embeddings
|
||||
|
||||
- Reduzir `batch-size` no `application.yml`
|
||||
- Verificar se o Ollama está usando GPU: `ollama run qwen3-embedding --verbose` e checar `eval rate`
|
||||
- Sem GPU, o modelo 8B pode ser lento para centenas de funções (esperar 2-5 minutos)
|
||||
|
||||
### Contexto da LLM estourando
|
||||
|
||||
- Reduzir `max-context-tokens` em `app.ai.rag`
|
||||
- Reduzir `max-history-messages` em `app.ai.chat`
|
||||
- Aumentar `chunk-max-chars` (para incluir menos chunks maiores)
|
||||
369
docs/ai/data-model.md
Normal file
369
docs/ai/data-model.md
Normal file
@ -0,0 +1,369 @@
|
||||
# Modelo de Dados — Módulo AI
|
||||
|
||||
## Diagrama Entidade-Relacionamento
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ TABELAS EXISTENTES │
|
||||
│ │
|
||||
│ binaries ──────┬── static_analysis ──┬── static_function │
|
||||
│ (workspace) │ ├── static_label │
|
||||
│ │ └── static_xref │
|
||||
│ │ │
|
||||
│ └── jobs │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
│ FK: binary_id
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ NOVAS TABELAS (V008) │
|
||||
│ │
|
||||
│ embedding_chunk ──── chat_session ──── chat_message │
|
||||
│ │ │ │
|
||||
│ │ │ FK: binary_id → binaries │
|
||||
│ │ │ FK: project_id → projects │
|
||||
│ │ │ │
|
||||
│ │ FK: binary_id │ │
|
||||
│ │ → binaries │ │
|
||||
│ │ │ │
|
||||
│ │ FK: source_id │ │
|
||||
│ │ → opcional │ │
|
||||
│ │ (static_func) │ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Migration V008 — DDL Completo
|
||||
|
||||
```sql
|
||||
-- V008__ai_embedding_and_chat.sql
|
||||
|
||||
-- Garantir que a extensão pgvector está ativada
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
|
||||
-- ============================================================
|
||||
-- Tabela: embedding_chunk
|
||||
-- Armazena chunks de texto embeddados para busca semântica
|
||||
-- ============================================================
|
||||
CREATE TABLE embedding_chunk (
|
||||
id UUID PRIMARY KEY,
|
||||
|
||||
-- Referência ao binário (obrigatória)
|
||||
binary_id UUID NOT NULL REFERENCES binaries(id) ON DELETE CASCADE,
|
||||
|
||||
-- Tipo de chunk: FUNCTION, METADATA
|
||||
chunk_type VARCHAR(30) NOT NULL,
|
||||
|
||||
-- Tipo da fonte: STATIC_FUNCTION, IMPORT, STRING (extensível)
|
||||
source_type VARCHAR(30),
|
||||
|
||||
-- FK opcional para a entidade fonte (ex: static_function.id)
|
||||
source_id UUID,
|
||||
|
||||
-- Texto completo que foi embeddado
|
||||
content TEXT NOT NULL,
|
||||
|
||||
-- Vetor de embedding (dimensão configurável, default 4096)
|
||||
-- qwen3-embedding:latest (8B) retorna 4096 dimensões
|
||||
embedding vector(4096),
|
||||
|
||||
-- Metadados flexíveis em JSON:
|
||||
-- {
|
||||
-- "function_name": "sub_401000",
|
||||
-- "address": "0x401000",
|
||||
-- "callers": ["WinMain", "sub_402300"],
|
||||
-- "callees": ["CreateFileA", "WriteFile"],
|
||||
-- "label_count": 2
|
||||
-- }
|
||||
metadata JSONB,
|
||||
|
||||
-- Contagem estimada de tokens (para controle de contexto)
|
||||
token_count INTEGER,
|
||||
|
||||
-- Timestamp de criação
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Índice para busca por binário (filtro de escopo)
|
||||
CREATE INDEX idx_embedding_chunk_binary ON embedding_chunk(binary_id);
|
||||
|
||||
-- Índice para lookup por fonte (ex: achar chunk de uma função específica)
|
||||
CREATE INDEX idx_embedding_chunk_source ON embedding_chunk(source_type, source_id);
|
||||
|
||||
-- Índice de busca textual (fallback para queries específicas)
|
||||
CREATE INDEX idx_embedding_chunk_content ON embedding_chunk
|
||||
USING gin (content gin_trgm_ops);
|
||||
|
||||
-- NOTA: Não há índice vetorial. HNSW e IVFFlat do pgvector limitados a 2000d.
|
||||
-- Sequential scan é rápido para < 10K vetores de 4096d.
|
||||
|
||||
-- ============================================================
|
||||
-- Tabela: chat_session
|
||||
-- Representa uma sessão de conversa com o chat IA
|
||||
-- ============================================================
|
||||
CREATE TABLE chat_session (
|
||||
id UUID PRIMARY KEY,
|
||||
|
||||
-- Binário em análise (escopo da conversa)
|
||||
binary_id UUID NOT NULL REFERENCES binaries(id) ON DELETE CASCADE,
|
||||
|
||||
-- Projeto pai (para navegação/breadcrumb)
|
||||
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
|
||||
-- Título da sessão (ex: "Analisando funções de rede")
|
||||
title VARCHAR(255),
|
||||
|
||||
-- Modelo de chat usado nesta sessão
|
||||
-- Ex: "gemma4:12b", "deepseek-chat", "gemma4:e4b"
|
||||
chat_model VARCHAR(100) NOT NULL,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_chat_session_binary ON chat_session(binary_id);
|
||||
|
||||
-- ============================================================
|
||||
-- Tabela: chat_message
|
||||
-- Mensagens individuais dentro de uma sessão
|
||||
-- ============================================================
|
||||
CREATE TABLE chat_message (
|
||||
id UUID PRIMARY KEY,
|
||||
|
||||
-- Sessão pai
|
||||
session_id UUID NOT NULL REFERENCES chat_session(id) ON DELETE CASCADE,
|
||||
|
||||
-- Papel do emissor: USER, ASSISTANT, SYSTEM
|
||||
role VARCHAR(20) NOT NULL,
|
||||
|
||||
-- Conteúdo textual da mensagem
|
||||
content TEXT NOT NULL,
|
||||
|
||||
-- Estimativa de tokens (para tracking de uso)
|
||||
token_count INTEGER,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Índice para buscar histórico em ordem cronológica
|
||||
CREATE INDEX idx_chat_message_session ON chat_message(session_id, created_at);
|
||||
```
|
||||
|
||||
## Entidades JPA
|
||||
|
||||
### `EmbeddingChunk`
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "embedding_chunk")
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class EmbeddingChunk {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@Column(name = "binary_id", nullable = false)
|
||||
private UUID binaryId;
|
||||
|
||||
@Column(name = "chunk_type", nullable = false, length = 30)
|
||||
private String chunkType;
|
||||
// Valores: "FUNCTION", "METADATA"
|
||||
|
||||
@Column(name = "source_type", length = 30)
|
||||
private String sourceType;
|
||||
// Valores: "STATIC_FUNCTION", null (para METADATA)
|
||||
|
||||
@Column(name = "source_id")
|
||||
private UUID sourceId;
|
||||
// FK lógica para static_function.id (sem constraint de FK no banco
|
||||
// porque pode ser null ou apontar para tabelas diferentes no futuro)
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
@JdbcTypeCode(SqlTypes.VECTOR)
|
||||
@Column(name = "embedding", columnDefinition = "vector(1024)")
|
||||
private float[] embedding;
|
||||
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
@Column(name = "metadata", columnDefinition = "jsonb")
|
||||
private String metadata;
|
||||
// JSON string: {"function_name":"...", "address":"...", "callers":[...], ...}
|
||||
|
||||
@Column(name = "token_count")
|
||||
private Integer tokenCount;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
}
|
||||
```
|
||||
|
||||
**Notas sobre o mapeamento**:
|
||||
- `embedding` usa `@JdbcTypeCode(SqlTypes.VECTOR)` — requer Hibernate 6.2+ (Spring Boot 4.0.6 inclui)
|
||||
- `metadata` é `String` com `@JdbcTypeCode(SqlTypes.JSON)` — Hibernate converte de/para JSONB automaticamente
|
||||
- `sourceId` é FK lógica (não constraint no banco) para permitir flexibilidade futura (apontar para outras tabelas além de `static_function`)
|
||||
|
||||
### `ChatSession`
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "chat_session")
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class ChatSession {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@Column(name = "binary_id", nullable = false)
|
||||
private UUID binaryId;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "binary_id", insertable = false, updatable = false)
|
||||
private Binary binary;
|
||||
|
||||
@Column(name = "project_id", nullable = false)
|
||||
private UUID projectId;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "project_id", insertable = false, updatable = false)
|
||||
private Project project;
|
||||
|
||||
@Column(length = 255)
|
||||
private String title;
|
||||
|
||||
@Column(name = "chat_model", nullable = false, length = 100)
|
||||
private String chatModel;
|
||||
|
||||
@OneToMany(mappedBy = "session", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@Builder.Default
|
||||
private List<ChatMessage> messages = new ArrayList<>();
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Instant updatedAt;
|
||||
}
|
||||
```
|
||||
|
||||
### `ChatMessage`
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "chat_message")
|
||||
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
|
||||
public class ChatMessage {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "session_id", nullable = false)
|
||||
private ChatSession session;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
private String role; // "USER", "ASSISTANT", "SYSTEM"
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
private String content;
|
||||
|
||||
@Column(name = "token_count")
|
||||
private Integer tokenCount;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
}
|
||||
```
|
||||
|
||||
## Relacionamentos
|
||||
|
||||
```
|
||||
chat_session (1) ────────── (N) chat_message
|
||||
│ │
|
||||
│ FK binary_id → binaries │ FK session_id → chat_session
|
||||
│ FK project_id → projects │
|
||||
│ │
|
||||
└── cascade: ALL, orphanRemoval └── cascade: ALL, orphanRemoval
|
||||
(deletar sessão deleta (deletar sessão deleta
|
||||
todas as mensagens) todas as mensagens)
|
||||
|
||||
embedding_chunk (N) ────────── (1) binaries
|
||||
│
|
||||
│ FK binary_id → binaries
|
||||
│ ON DELETE CASCADE (deletar binário deleta todos os chunks)
|
||||
│
|
||||
└── source_id: FK lógica para static_function.id
|
||||
(não é constraint; usado para join manual)
|
||||
```
|
||||
|
||||
## Índices e Performance
|
||||
|
||||
### Índices
|
||||
|
||||
| Índice | Coluna(s) | Uso |
|
||||
|---|---|---|
|
||||
| `idx_embedding_chunk_binary` | `binary_id` | Filtrar chunks por binário (todas as queries) |
|
||||
| `idx_embedding_chunk_source` | `source_type, source_id` | Lookup por fonte (ex: achar chunk de uma função específica) |
|
||||
| `idx_embedding_chunk_content` | `content` (GIN trgm) | Busca textual de fallback (`ILIKE '%term%'`) |
|
||||
| `idx_chat_session_binary` | `binary_id` | Listar sessões de um binário |
|
||||
| `idx_chat_message_session` | `session_id, created_at` | Buscar histórico em ordem cronológica |
|
||||
|
||||
> **Performance**: Sem índice vetorial (HNSW/IVFFlat limitados a 2000d). Para < 10K vetores de 4096d, sequential scan < 100ms.
|
||||
|
||||
## Coluna `metadata` (JSONB)
|
||||
|
||||
O campo `metadata` armazena informações estruturadas que não fazem parte do texto embeddado mas são úteis para filtragem e exibição:
|
||||
|
||||
```json
|
||||
{
|
||||
"function_name": "sub_401000",
|
||||
"address": "0x401000",
|
||||
"callers": ["WinMain", "sub_402300"],
|
||||
"callees": ["CreateFileA", "WriteFile", "CloseHandle"],
|
||||
"label_count": 2,
|
||||
"assembly_length": 1247,
|
||||
"has_decompiled_code": false
|
||||
}
|
||||
```
|
||||
|
||||
Para chunks do tipo `METADATA`:
|
||||
|
||||
```json
|
||||
{
|
||||
"filename": "malware.exe",
|
||||
"format": "PE32",
|
||||
"architecture": "x86",
|
||||
"compiler": "MSVC 19.0",
|
||||
"function_count": 342
|
||||
}
|
||||
```
|
||||
|
||||
Vantagens do JSONB sobre colunas separadas:
|
||||
- Flexível: adicionar campos sem migration
|
||||
- Indexável: `CREATE INDEX ... ON embedding_chunk USING gin (metadata jsonb_path_ops)` se necessário
|
||||
- Permite queries como: `SELECT * FROM embedding_chunk WHERE metadata->>'function_name' = 'WinMain'`
|
||||
|
||||
## Políticas de Deleção
|
||||
|
||||
| Operação | Comportamento |
|
||||
|---|---|
|
||||
| Deletar binário | `ON DELETE CASCADE` → deleta todos os `embedding_chunk`, `chat_session` e `chat_message` |
|
||||
| Deletar projeto | `ON DELETE CASCADE` via binário → cascata completa |
|
||||
| Re-analisar binário | Hard delete de `embedding_chunk` via `deleteByBinaryId()` antes de reindexar |
|
||||
| Deletar sessão | `orphanRemoval = true` → deleta todas as `chat_message` |
|
||||
|
||||
## Relação com o Schema Existente
|
||||
|
||||
A migration V008 é aditiva — não altera nenhuma tabela existente. As novas tabelas referenciam:
|
||||
- `binaries(id)` — FK com CASCADE
|
||||
- `projects(id)` — FK com CASCADE
|
||||
|
||||
O `source_id` em `embedding_chunk` referencia logicamente `static_function.id`, mas **sem constraint de FK** para:
|
||||
- Permitir valores nulos (chunk METADATA não tem source)
|
||||
- Permitir referências futuras a outras tabelas (ex: tabela de strings, tabela de imports)
|
||||
671
docs/ai/frontend-guide.md
Normal file
671
docs/ai/frontend-guide.md
Normal file
@ -0,0 +1,671 @@
|
||||
# Guia de Implementação Frontend — Chat IA (RAG)
|
||||
|
||||
## Visão Geral
|
||||
|
||||
Este documento descreve como integrar o frontend com a API de chat IA do Decompile-AI. O fluxo consiste em:
|
||||
|
||||
1. Após upload + análise estática de um binário, embeddings são gerados automaticamente
|
||||
2. O usuário cria uma sessão de chat vinculada ao binário
|
||||
3. Mensagens são enviadas e a resposta da LLM é recebida via **Server-Sent Events (SSE)**
|
||||
4. O backend faz RAG automaticamente: busca contexto relevante no binário e injeta no prompt
|
||||
|
||||
## Stack Recomendada (Frontend)
|
||||
|
||||
| Tecnologia | Propósito |
|
||||
|---|---|
|
||||
| **EventSource** ou **fetch + ReadableStream** | Consumir SSE stream |
|
||||
| **React** (ou framework similar) | UI components |
|
||||
| **AbortController** | Cancelar stream em andamento |
|
||||
| **@microsoft/fetch-event-source** | Lib opcional para SSE com POST + cancelamento |
|
||||
|
||||
---
|
||||
|
||||
## 1. Endpoints da API
|
||||
|
||||
### 1.1 Criar Sessão de Chat
|
||||
|
||||
```
|
||||
POST /binaries/{binaryId}/chat/sessions?projectId={projectId}&model={model}
|
||||
```
|
||||
|
||||
**Query Params**:
|
||||
|
||||
| Parâmetro | Tipo | Obrigatório | Descrição |
|
||||
|---|---|---|---|
|
||||
| `projectId` | UUID | SIM | ID do projeto pai |
|
||||
| `model` | String | Não | Modelo LLM (`gemma4:12b` padrão) |
|
||||
|
||||
**Response** (`201 Created`):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"binaryId": "660e8400-e29b-41d4-a716-446655440001",
|
||||
"projectId": "770e8400-e29b-41d4-a716-446655440002",
|
||||
"title": "malware.exe — Chat",
|
||||
"chatModel": "gemma4:12b",
|
||||
"messageCount": 0,
|
||||
"createdAt": "2026-06-08T15:30:00Z",
|
||||
"updatedAt": "2026-06-08T15:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Erros**:
|
||||
|
||||
| Código | Significado |
|
||||
|---|---|
|
||||
| `404` | Binário/projeto não encontrado |
|
||||
| `400` | Modelo indisponível (Ollama não está rodando) |
|
||||
| `400` | `projectId` ausente |
|
||||
|
||||
### 1.2 Listar Sessões do Binário
|
||||
|
||||
```
|
||||
GET /binaries/{binaryId}/chat/sessions
|
||||
```
|
||||
|
||||
**Response** (`200 OK`):
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "550e8400-...",
|
||||
"binaryId": "660e8400-...",
|
||||
"projectId": "770e8400-...",
|
||||
"title": "malware.exe — Chat",
|
||||
"chatModel": "gemma4:12b",
|
||||
"messageCount": 5,
|
||||
"createdAt": "2026-06-08T15:30:00Z",
|
||||
"updatedAt": "2026-06-08T16:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Ordenado por `updatedAt` decrescente (sessão mais recente primeiro).
|
||||
|
||||
### 1.3 Ver Detalhes da Sessão
|
||||
|
||||
```
|
||||
GET /chat/sessions/{sessionId}
|
||||
```
|
||||
|
||||
Mesmo formato de resposta que o item 1.1.
|
||||
|
||||
### 1.4 Deletar Sessão
|
||||
|
||||
```
|
||||
DELETE /chat/sessions/{sessionId}
|
||||
```
|
||||
|
||||
**Response**: `204 No Content`.
|
||||
|
||||
> Deletar a sessão remove **todas** as mensagens associadas (cascade).
|
||||
|
||||
### 1.5 Histórico de Mensagens
|
||||
|
||||
```
|
||||
GET /chat/sessions/{sessionId}/messages
|
||||
```
|
||||
|
||||
**Response** (`200 OK`):
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "aa0e8400-...",
|
||||
"sessionId": "550e8400-...",
|
||||
"role": "USER",
|
||||
"content": "Quais funções lidam com rede?",
|
||||
"tokenCount": 12,
|
||||
"createdAt": "2026-06-08T15:30:30Z"
|
||||
},
|
||||
{
|
||||
"id": "bb0e8400-...",
|
||||
"sessionId": "550e8400-...",
|
||||
"role": "ASSISTANT",
|
||||
"content": "O binário importa funções de rede da WS2_32.dll...",
|
||||
"tokenCount": 85,
|
||||
"createdAt": "2026-06-08T15:30:35Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Ordenado por `createdAt` crescente (mais antigo primeiro).
|
||||
|
||||
### 1.6 Enviar Mensagem (SSE Streaming)
|
||||
|
||||
```
|
||||
POST /chat/sessions/{sessionId}/messages
|
||||
Content-Type: application/json
|
||||
Accept: text/event-stream
|
||||
```
|
||||
|
||||
**Request Body**:
|
||||
|
||||
```json
|
||||
{
|
||||
"content": "Quais funções nesse binário lidam com criptografia?"
|
||||
}
|
||||
```
|
||||
|
||||
| Campo | Tipo | Validação |
|
||||
|---|---|---|
|
||||
| `content` | String | 1–4000 caracteres, obrigatório |
|
||||
|
||||
**Response**: `200 OK` com `Content-Type: text/event-stream`
|
||||
|
||||
---
|
||||
|
||||
## 2. Protocolo SSE (Server-Sent Events)
|
||||
|
||||
O stream é uma sequência de eventos `data:` enviados ao longo da conexão HTTP.
|
||||
|
||||
### 2.1 Tipos de Evento
|
||||
|
||||
| `type` | Quando | Campos |
|
||||
|---|---|---|
|
||||
| `chunk` | Token de texto gerado pela LLM | `content: String` |
|
||||
| `done` | Stream concluído com sucesso | `sessionId: UUID` |
|
||||
| `error` | Erro durante o processamento | `message: String` |
|
||||
|
||||
### 2.2 Exemplo de Stream
|
||||
|
||||
```
|
||||
data: {"type":"chunk","content":"O binário"}
|
||||
|
||||
data: {"type":"chunk","content":" utiliza "}
|
||||
|
||||
data: {"type":"chunk","content":"as seguintes"}
|
||||
|
||||
data: {"type":"chunk","content":" funções"}
|
||||
|
||||
data: {"type":"chunk","content":" para criptografia:"}
|
||||
|
||||
data: {"type":"chunk","content":"\n\n1. **sub_405000**"}
|
||||
|
||||
...
|
||||
|
||||
data: {"type":"done","sessionId":"550e8400-e29b-..."}
|
||||
```
|
||||
|
||||
### 2.3 Evento de Erro
|
||||
|
||||
```
|
||||
data: {"type":"error","message":"Ollama is not available. Install ollama and pull gemma4:12b."}
|
||||
```
|
||||
|
||||
Após um evento `error`, a conexão é fechada. Nenhum evento `done` é enviado.
|
||||
|
||||
### 2.4 Notas sobre SSE
|
||||
|
||||
- A conexão é mantida aberta até o stream terminar (`done` ou `error`)
|
||||
- Tokens podem chegar com latência variável (a LLM gera em tempo real)
|
||||
- Cada evento `data:` contém uma linha JSON válida
|
||||
- O frontend deve acumular os `chunk.content` para montar a resposta completa
|
||||
- O backend **já salva** a resposta completa no banco ao final — o frontend não precisa reenviar
|
||||
|
||||
---
|
||||
|
||||
## 3. Fluxo de Implementação
|
||||
|
||||
### 3.1 Diagrama de Sequência
|
||||
|
||||
```
|
||||
Frontend Backend Ollama
|
||||
│ │ │
|
||||
│ POST /chat/sessions (criar) │ │
|
||||
├────────────────────────────────►│ │
|
||||
│◄────────────────────────────────┤ sessionId │
|
||||
│ │ │
|
||||
│ POST /chat/sessions/{id}/messages (SSE) │
|
||||
├────────────────────────────────►│ │
|
||||
│ │ embed query │
|
||||
│ ├──────────────────────────────►│
|
||||
│ │◄──────────────────────────────┤
|
||||
│ │ │
|
||||
│ │ pgvector search (RAG) │
|
||||
│ │ build system prompt │
|
||||
│ │ │
|
||||
│ │ stream LLM │
|
||||
│ ├──────────────────────────────►│
|
||||
│◄──── data: {"type":"chunk"...}──┤◄──────────────────────────────┤
|
||||
│◄──── data: {"type":"chunk"...}──┤◄──────────────────────────────┤
|
||||
│◄──── data: {"type":"chunk"...}──┤◄──────────────────────────────┤
|
||||
│◄──── data: {"type":"done"...}───┤ │
|
||||
│ │ │
|
||||
│ │ save assistant message │
|
||||
│ │ │
|
||||
│ GET /chat/sessions/{id}/messages (refresh) │
|
||||
├────────────────────────────────►│ │
|
||||
│◄────────────────────────────────┤ full history │
|
||||
```
|
||||
|
||||
### 3.2 Estrutura de Componentes (React)
|
||||
|
||||
```
|
||||
BinaryDetailPage
|
||||
├── ChatPanel ← componente principal do chat
|
||||
│ ├── ChatSessionList ← barra lateral: lista de sessões
|
||||
│ │ ├── ChatSessionItem ← cada sessão (título, modelo, msg count)
|
||||
│ │ └── NewSessionButton ← botão "Nova conversa"
|
||||
│ │
|
||||
│ └── ChatWindow ← área principal de conversa
|
||||
│ ├── ChatMessageList ← scroll area com mensagens
|
||||
│ │ ├── UserMessage ← bolha do usuário
|
||||
│ │ └── AssistantMessage ← bolha do assistente (com markdown)
|
||||
│ │
|
||||
│ ├── StreamingMessage ← mensagem sendo gerada (cursor piscando)
|
||||
│ └── ChatInput ← textarea + botão enviar
|
||||
```
|
||||
|
||||
### 3.3 Exemplo de Hook: `useChatSession`
|
||||
|
||||
```typescript
|
||||
interface ChatSession {
|
||||
id: string;
|
||||
binaryId: string;
|
||||
projectId: string;
|
||||
title: string;
|
||||
chatModel: string;
|
||||
messageCount: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
role: 'USER' | 'ASSISTANT';
|
||||
content: string;
|
||||
tokenCount: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
function useChatSession(binaryId: string, projectId: string) {
|
||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||
const [streamingContent, setStreamingContent] = useState('');
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
// Listar sessões
|
||||
const loadSessions = async () => {
|
||||
const res = await fetch(`/binaries/${binaryId}/chat/sessions`);
|
||||
const data = await res.json();
|
||||
setSessions(data);
|
||||
};
|
||||
|
||||
// Criar nova sessão
|
||||
const createSession = async (model: string = 'gemma4:12b') => {
|
||||
const res = await fetch(
|
||||
`/binaries/${binaryId}/chat/sessions?projectId=${projectId}&model=${model}`,
|
||||
{ method: 'POST' }
|
||||
);
|
||||
const session = await res.json();
|
||||
setSessions(prev => [session, ...prev]);
|
||||
setActiveSessionId(session.id);
|
||||
setMessages([]);
|
||||
return session;
|
||||
};
|
||||
|
||||
// Carregar histórico
|
||||
const loadMessages = async (sessionId: string) => {
|
||||
const res = await fetch(`/chat/sessions/${sessionId}/messages`);
|
||||
const data = await res.json();
|
||||
setMessages(data);
|
||||
};
|
||||
|
||||
// Enviar mensagem (SSE streaming)
|
||||
const sendMessage = async (content: string) => {
|
||||
if (!activeSessionId) return;
|
||||
|
||||
// Adiciona mensagem do usuário ao estado local (otimista)
|
||||
const userMsg: ChatMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
sessionId: activeSessionId,
|
||||
role: 'USER',
|
||||
content,
|
||||
tokenCount: 0,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
setMessages(prev => [...prev, userMsg]);
|
||||
setStreamingContent('');
|
||||
setIsStreaming(true);
|
||||
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/chat/sessions/${activeSessionId}/messages`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream',
|
||||
},
|
||||
body: JSON.stringify({ content }),
|
||||
signal: controller.signal,
|
||||
}
|
||||
);
|
||||
|
||||
const reader = response.body!.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let fullContent = '';
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('data: ')) continue;
|
||||
const event = JSON.parse(line.slice(6));
|
||||
|
||||
if (event.type === 'chunk') {
|
||||
fullContent += event.content;
|
||||
setStreamingContent(fullContent);
|
||||
} else if (event.type === 'done') {
|
||||
setIsStreaming(false);
|
||||
// Recarrega histórico real para pegar IDs do backend
|
||||
await loadMessages(activeSessionId);
|
||||
} else if (event.type === 'error') {
|
||||
setIsStreaming(false);
|
||||
throw new Error(event.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.name !== 'AbortError') {
|
||||
setIsStreaming(false);
|
||||
console.error('Chat error:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Cancelar stream
|
||||
const cancelStream = () => {
|
||||
abortRef.current?.abort();
|
||||
setIsStreaming(false);
|
||||
};
|
||||
|
||||
// Deletar sessão
|
||||
const deleteSession = async (sessionId: string) => {
|
||||
await fetch(`/chat/sessions/${sessionId}`, { method: 'DELETE' });
|
||||
setSessions(prev => prev.filter(s => s.id !== sessionId));
|
||||
if (activeSessionId === sessionId) {
|
||||
setActiveSessionId(null);
|
||||
setMessages([]);
|
||||
}
|
||||
};
|
||||
|
||||
// Selecionar sessão
|
||||
const selectSession = async (sessionId: string) => {
|
||||
setActiveSessionId(sessionId);
|
||||
await loadMessages(sessionId);
|
||||
};
|
||||
|
||||
return {
|
||||
sessions,
|
||||
activeSessionId,
|
||||
messages,
|
||||
streamingContent,
|
||||
isStreaming,
|
||||
loadSessions,
|
||||
createSession,
|
||||
selectSession,
|
||||
sendMessage,
|
||||
cancelStream,
|
||||
deleteSession,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 Componente de Chat (React)
|
||||
|
||||
```tsx
|
||||
function ChatWindow({
|
||||
messages,
|
||||
streamingContent,
|
||||
isStreaming,
|
||||
onSend,
|
||||
onCancel,
|
||||
}: {
|
||||
messages: ChatMessage[];
|
||||
streamingContent: string;
|
||||
isStreaming: boolean;
|
||||
onSend: (content: string) => void;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const [input, setInput] = useState('');
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [messages, streamingContent]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!input.trim() || isStreaming) return;
|
||||
onSend(input.trim());
|
||||
setInput('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="chat-window">
|
||||
<div className="messages">
|
||||
{messages.map(msg => (
|
||||
<MessageBubble key={msg.id} role={msg.role} content={msg.content} />
|
||||
))}
|
||||
{isStreaming && (
|
||||
<MessageBubble
|
||||
role="ASSISTANT"
|
||||
content={streamingContent}
|
||||
isStreaming
|
||||
/>
|
||||
)}
|
||||
<div ref={scrollRef} />
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="chat-input">
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
placeholder="Pergunte sobre funções, protocolos, estruturas..."
|
||||
disabled={isStreaming}
|
||||
rows={2}
|
||||
/>
|
||||
{isStreaming ? (
|
||||
<button type="button" onClick={onCancel}>Parar</button>
|
||||
) : (
|
||||
<button type="submit" disabled={!input.trim()}>Enviar</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Tratamento de Erros
|
||||
|
||||
| Cenário | Código | Ação Recomendada |
|
||||
|---|---|---|
|
||||
| **Sessão não encontrada** | 404 | Redirecionar para lista de binários |
|
||||
| **Binário não analisado** | — | Embeddings não gerados → exibir mensagem "Execute a análise estática primeiro" |
|
||||
| **Ollama offline** | `error` SSE | Exibir "Ollama não está rodando. Inicie com `ollama serve`" |
|
||||
| **Modelo não baixado** | `error` SSE | Exibir "Modelo não encontrado. Execute `ollama pull gemma4:12b`" |
|
||||
| **Timeout do stream** | Conexão fechada | Mostrar mensagem parcial + botão "Tentar novamente" |
|
||||
| **Conteúdo vazio** | 400 | Validar no frontend: mínimo 1 caractere |
|
||||
| **Conteúdo > 4000 chars** | 400 | Limitar textarea a 4000 caracteres |
|
||||
|
||||
### Exemplo de Error Boundary
|
||||
|
||||
```typescript
|
||||
function getErrorMessage(error: string): string {
|
||||
if (error.includes('Ollama is not available')) {
|
||||
return 'Servidor LLM local (Ollama) não está rodando. Execute `ollama serve` no terminal.';
|
||||
}
|
||||
if (error.includes('Install ollama and pull')) {
|
||||
return `Modelo LLM não encontrado. Execute no terminal:\nollama pull gemma4:12b`;
|
||||
}
|
||||
return `Erro ao processar a resposta: ${error}`;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Considerações de UX
|
||||
|
||||
### 5.1 Estados da UI
|
||||
|
||||
| Estado | O que mostrar |
|
||||
|---|---|
|
||||
| **Binário sem análise** | "Execute a análise estática antes de usar o chat." |
|
||||
| **Sem embeddings** | "Aguarde a geração dos embeddings..." (polling do job `GENERATE_EMBEDDINGS`) |
|
||||
| **Nenhuma sessão** | Botão "Iniciar conversa" proeminente |
|
||||
| **Streaming** | Bolha do assistente com cursor piscando (`▊`), botão "Parar" |
|
||||
| **Stream concluído** | Mensagem completa renderizada com markdown |
|
||||
| **Erro** | Banner de erro com ação sugerida |
|
||||
|
||||
### 5.2 Renderização de Markdown
|
||||
|
||||
A resposta da LLM frequentemente contém markdown (negrito, listas, blocos de código). Use uma biblioteca como `react-markdown` com syntax highlighting para assembly:
|
||||
|
||||
```tsx
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
|
||||
function MessageBubble({ content, role, isStreaming }: Props) {
|
||||
return (
|
||||
<div className={`bubble ${role.toLowerCase()}`}>
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
code({ className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return match ? (
|
||||
<SyntaxHighlighter language={match[1]} PreTag="div">
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>
|
||||
) : (
|
||||
<code className={className} {...props}>{children}</code>
|
||||
);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
{isStreaming && <span className="cursor">▊</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Gerenciamento de Scroll
|
||||
|
||||
- Scroll automático para o final ao receber novos tokens
|
||||
- Se o usuário scrollar para cima manualmente, **não** força scroll (respeita leitura)
|
||||
- Retoma auto-scroll quando usuário scrolla de volta ao final
|
||||
|
||||
```typescript
|
||||
function useAutoScroll(deps: any[]) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [userScrolledUp, setUserScrolledUp] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const handleScroll = () => {
|
||||
const isAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 50;
|
||||
setUserScrolledUp(!isAtBottom);
|
||||
};
|
||||
|
||||
el.addEventListener('scroll', handleScroll);
|
||||
return () => el.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userScrolledUp) {
|
||||
containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' });
|
||||
}
|
||||
}, deps);
|
||||
|
||||
return containerRef;
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 Polling de Status dos Embeddings
|
||||
|
||||
Antes de habilitar o chat, verifique se os embeddings foram gerados:
|
||||
|
||||
```typescript
|
||||
async function hasEmbeddings(binaryId: string): Promise<boolean> {
|
||||
// Verifica se existem chunks para este binário
|
||||
// Opção 1: endpoint dedicado (a ser adicionado)
|
||||
// Opção 2: verificar jobs — se existe GENERATE_EMBEDDINGS COMPLETED
|
||||
|
||||
const res = await fetch(`/jobs?binaryId=${binaryId}&type=GENERATE_EMBEDDINGS&status=COMPLETED`);
|
||||
const jobs = await res.json();
|
||||
return jobs.length > 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Resumo de Integração
|
||||
|
||||
```
|
||||
Preparação:
|
||||
1. Upload binário → POST /projects/{id}/binaries?engine=IDA5
|
||||
2. Aguardar análise → polling GET /jobs?binaryId={id}&type=STATIC_ANALYSIS
|
||||
3. Aguardar embeddings → polling GET /jobs?binaryId={id}&type=GENERATE_EMBEDDINGS
|
||||
|
||||
Chat:
|
||||
4. Criar sessão → POST /binaries/{id}/chat/sessions?projectId={pid}
|
||||
5. Enviar mensagem → POST /chat/sessions/{sid}/messages (SSE stream)
|
||||
6. Renderizar tokens → acumular data.type=chunk
|
||||
7. Finalizar → data.type=done → recarregar histórico
|
||||
8. Histórico → GET /chat/sessions/{sid}/messages (ao voltar à sessão)
|
||||
|
||||
Gerenciamento:
|
||||
9. Listar sessões → GET /binaries/{id}/chat/sessions
|
||||
10. Deletar sessão → DELETE /chat/sessions/{sid}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Endpoints de Suporte
|
||||
|
||||
Estes endpoints **já existem** e são úteis para polling de status:
|
||||
|
||||
### Jobs
|
||||
|
||||
```
|
||||
GET /jobs?projectId={projectId}&type=GENERATE_EMBEDDINGS&status=COMPLETED
|
||||
```
|
||||
|
||||
Retorna jobs do tipo `GENERATE_EMBEDDINGS` concluídos. Use para verificar se os embeddings estão prontos.
|
||||
|
||||
```
|
||||
GET /jobs?binaryId={binaryId}&type=GENERATE_EMBEDDINGS
|
||||
```
|
||||
|
||||
Status possíveis: `ENQUEUED`, `STARTED`, `IN_PROGRESS`, `COMPLETED`, `FAILED`.
|
||||
|
||||
### Funções (para referência no chat)
|
||||
|
||||
```
|
||||
GET /analysis/{analysisId}/functions?page=0&size=50
|
||||
GET /functions/{functionId}
|
||||
GET /functions/{functionId}/callers
|
||||
GET /functions/{functionId}/callees
|
||||
```
|
||||
|
||||
O frontend pode usar esses endpoints para permitir que o usuário **clique em uma função mencionada na resposta** e veja seus detalhes (assembly, xrefs, labels).
|
||||
424
docs/ai/integration-points.md
Normal file
424
docs/ai/integration-points.md
Normal file
@ -0,0 +1,424 @@
|
||||
# Pontos de Integração — Módulo AI com Sistema Existente
|
||||
|
||||
## Visão Geral
|
||||
|
||||
O módulo `ai/` não existe isoladamente — ele se integra profundamente com os módulos existentes (`workspace`, `analysis`, `job`) através de eventos de domínio, dependências Modulith e jobs assíncronos.
|
||||
|
||||
Este documento descreve cada ponto de integração, o fluxo de dados e as modificações necessárias nos módulos existentes.
|
||||
|
||||
---
|
||||
|
||||
## 1. Diagrama de Sequência Completo
|
||||
|
||||
```
|
||||
Usuário Controller AnalysisSvc JobModule AI Module Ollama/LLM
|
||||
│ │ │ │ │ │
|
||||
│ POST /upload │ │ │ │ │
|
||||
├────────────────►│ │ │ │ │
|
||||
│ │ save │ │ │ │
|
||||
│ ├─────────────►│ │ │ │
|
||||
│ │ │ BinaryUp- │ │ │
|
||||
│ │ │ loadedEvent │ │ │
|
||||
│ │ ├─────────────►│ │ │
|
||||
│ │ │ │ ANALYZE_FILE │ │
|
||||
│ │ │ │ (DiE Docker) │ │
|
||||
│ │ │ │ │ │
|
||||
│ POST /analyze │ │ │ │ │
|
||||
├────────────────►│ │ │ │ │
|
||||
│ │ │ StaticAnlys │ │ │
|
||||
│ │ │ RequestedEvt │ │ │
|
||||
│ │ ├─────────────►│ │ │
|
||||
│ │ │ │ STATIC_ANALYS │ │
|
||||
│ │ │ │ (IDA5 Docker) │ │
|
||||
│ │ │◄─────────────┤ saveResult() │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ StaticAnlys │ │ │
|
||||
│ │ │ CompletedEvt │ │ │
|
||||
│ │ ├─────────────►│ │ │
|
||||
│ │ │ │ GENERATE_EMB │ │
|
||||
│ │ │ ├──────────────►│ │
|
||||
│ │ │ │ │ indexBinary() │
|
||||
│ │ │ │ ├──────────────►│
|
||||
│ │ │ │ │◄──────────────┤
|
||||
│ │ │ │◄──────────────┤ embeddings │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ (binário indexado) │
|
||||
│ │ │ │ │ │
|
||||
│ POST /chat/... │ │ │ │ │
|
||||
├────────────────►│ │ │ │ │
|
||||
│ │ │ │ │ chat() │
|
||||
│ │ │ │ ├──────────────►│
|
||||
│ │◄─────────────┼──────────────┼───────────────┤ SSE stream │
|
||||
│◄────────────────┤ │ │ │ │
|
||||
```
|
||||
|
||||
## 2. Eventos
|
||||
|
||||
### 2.1 Novo Evento: `StaticAnalysisCompletedEvent`
|
||||
|
||||
**Definição**: `analysis/event/StaticAnalysisCompletedEvent.java`
|
||||
|
||||
```java
|
||||
package ai.decompile.analysis.event;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Published when a static analysis job completes successfully.
|
||||
* Triggers the embedding generation pipeline.
|
||||
*/
|
||||
public record StaticAnalysisCompletedEvent(
|
||||
UUID binaryId,
|
||||
UUID projectId,
|
||||
UUID workspaceId,
|
||||
UUID analysisId,
|
||||
String engine
|
||||
) {}
|
||||
```
|
||||
|
||||
**Quem publica**: `StaticAnalysisHandler.process()` no módulo `job`
|
||||
|
||||
**Quem consome**: `EmbeddingEventSubscriber` no módulo `job`
|
||||
|
||||
**Momento da publicação**: Após `analysisService.saveResult()` e `binaryService.markStaticAnalysisDone()` concluírem com sucesso.
|
||||
|
||||
### 2.2 Publicação no `StaticAnalysisHandler`
|
||||
|
||||
**Arquivo a modificar**: `job/service/handler/StaticAnalysisHandler.java`
|
||||
|
||||
**Mudança**: Adicionar publicação do evento após salvar resultados:
|
||||
|
||||
```java
|
||||
// Dentro de process(), após saveResult() e markStaticAnalysisDone():
|
||||
@Autowired
|
||||
private ApplicationEventPublisher eventPublisher;
|
||||
|
||||
// ... no final de process():
|
||||
var binary = binaryService.getBinary(job.getBinaryId());
|
||||
eventPublisher.publishEvent(
|
||||
new StaticAnalysisCompletedEvent(
|
||||
binary.getId(),
|
||||
binary.getProject().getId(),
|
||||
binary.getProject().getWorkspace().getId(),
|
||||
analysisResponse.id(),
|
||||
engineName
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
> **Alternativa com Spring Modulith**: Usar `@ApplicationModuleListener` exige que o evento seja publicado via `ApplicationEventPublisher` do Spring. O Spring Modulith intercepta e persiste na tabela `event_publication` para garantia de entrega.
|
||||
|
||||
### 2.3 Novo Subscriber: `EmbeddingEventSubscriber`
|
||||
|
||||
**Arquivo a criar**: `job/service/EmbeddingEventSubscriber.java`
|
||||
|
||||
```java
|
||||
package ai.decompile.job.service;
|
||||
|
||||
import ai.decompile.analysis.event.StaticAnalysisCompletedEvent;
|
||||
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 EmbeddingEventSubscriber {
|
||||
|
||||
private final JobRepository jobRepository;
|
||||
private final JobMessagePublisher jobMessagePublisher;
|
||||
|
||||
@ApplicationModuleListener
|
||||
public void on(StaticAnalysisCompletedEvent event) {
|
||||
log.info(">>> EmbeddingEventSubscriber: Received StaticAnalysisCompletedEvent binary={}", event.binaryId());
|
||||
|
||||
var job = createJob(event);
|
||||
log.info(">>> EmbeddingEventSubscriber: Created GENERATE_EMBEDDINGS job {} for binary {}", job.getId(), event.binaryId());
|
||||
jobMessagePublisher.publish(job.getId());
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
Job createJob(StaticAnalysisCompletedEvent event) {
|
||||
return jobRepository.save(
|
||||
Job.builder()
|
||||
.type(JobType.GENERATE_EMBEDDINGS)
|
||||
.status(JobStatus.ENQUEUED)
|
||||
.projectId(event.projectId())
|
||||
.binaryId(event.binaryId())
|
||||
.payload("{\"analysisId\":\"" + event.analysisId() + "\"}")
|
||||
.retryCount(0)
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Jobs
|
||||
|
||||
### 3.1 Novo `JobType`
|
||||
|
||||
**Arquivo a modificar**: `job/model/enums/JobType.java`
|
||||
|
||||
```java
|
||||
public enum JobType {
|
||||
ANALYZE_FILE,
|
||||
STATIC_ANALYSIS,
|
||||
GENERATE_EMBEDDINGS // <-- novo
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Novo Handler: `EmbeddingGenerationHandler`
|
||||
|
||||
**Arquivo a criar**: `job/service/handler/EmbeddingGenerationHandler.java`
|
||||
|
||||
```java
|
||||
package ai.decompile.job.service.handler;
|
||||
|
||||
import ai.decompile.ai.service.EmbeddingService;
|
||||
import ai.decompile.job.model.entity.Job;
|
||||
import ai.decompile.job.model.enums.JobStatus;
|
||||
import ai.decompile.job.model.enums.JobType;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Log4j2
|
||||
@Component
|
||||
public class EmbeddingGenerationHandler implements JobHandler {
|
||||
|
||||
private final EmbeddingService embeddingService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public EmbeddingGenerationHandler(EmbeddingService embeddingService, ObjectMapper objectMapper) {
|
||||
this.embeddingService = embeddingService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(Job job) {
|
||||
log.info("Processing GENERATE_EMBEDDINGS job {} for binary {}", job.getId(), job.getBinaryId());
|
||||
|
||||
try {
|
||||
int chunks = embeddingService.indexBinary(job.getBinaryId());
|
||||
|
||||
job.setResult("{\"chunkCount\":" + chunks + "}");
|
||||
job.setStatus(JobStatus.COMPLETED);
|
||||
|
||||
log.info("Job {} completed. Generated {} embedding chunks for binary {}",
|
||||
job.getId(), chunks, job.getBinaryId());
|
||||
} catch (Exception e) {
|
||||
log.error("Job {} failed: {}", job.getId(), e.getMessage(), e);
|
||||
job.setStatus(JobStatus.FAILED);
|
||||
job.setErrorMessage(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JobType supportedType() {
|
||||
return JobType.GENERATE_EMBEDDINGS;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Nota**: `EmbeddingGenerationHandler` não precisa de `@ConditionalOnProperty` como `StaticAnalysisHandler`, pois embeddings não dependem do Docker. Ollama roda no host.
|
||||
|
||||
### 3.3 Atualizar `JobHandlerFactory`
|
||||
|
||||
Nenhuma mudança necessária — o factory já descobre automaticamente todos os beans `JobHandler` via injeção de lista.
|
||||
|
||||
## 4. Dependências Modulith
|
||||
|
||||
### 4.1 Atualizar `job/package-info.java`
|
||||
|
||||
**Arquivo a modificar**: `job/package-info.java`
|
||||
|
||||
```java
|
||||
@org.springframework.modulith.ApplicationModule(
|
||||
displayName = "Job",
|
||||
allowedDependencies = {
|
||||
"workspace::events",
|
||||
"workspace::entities",
|
||||
"workspace::services",
|
||||
"die::service",
|
||||
"die::model",
|
||||
"analysis::events",
|
||||
"analysis::services",
|
||||
"analysis::dto",
|
||||
"engine::services",
|
||||
"ai::services", // <-- novo: EmbeddingService
|
||||
"ai::model::entity", // <-- novo: EmbeddingChunk (se necessário)
|
||||
"common"
|
||||
})
|
||||
package ai.decompile.job;
|
||||
```
|
||||
|
||||
### 4.2 `analysis/package-info.java`
|
||||
|
||||
Não precisa ser alterado. O evento `StaticAnalysisCompletedEvent` é definido no próprio módulo `analysis` (`analysis.event`), então não há nova dependência.
|
||||
|
||||
### 4.3 `ai/package-info.java` (novo)
|
||||
|
||||
```java
|
||||
@org.springframework.modulith.ApplicationModule(
|
||||
displayName = "AI",
|
||||
allowedDependencies = {
|
||||
"workspace::entities", // Binary, Project (para JPA relationships)
|
||||
"workspace::services", // BinaryService (para buscar filePath, metadados)
|
||||
"analysis::entities", // StaticFunction, StaticAnalysis, StaticXref, StaticLabel
|
||||
"analysis::services", // AnalysisService (para buscar funções/xrefs)
|
||||
"common" // exceções, config
|
||||
})
|
||||
package ai.decompile.ai;
|
||||
```
|
||||
|
||||
## 5. Modificações em Arquivos Existentes
|
||||
|
||||
### 5.1 `pom.xml`
|
||||
|
||||
Adicionar dependências Spring AI (Spring AI BOM, ollama, openai, pgvector-store).
|
||||
|
||||
### 5.2 `application.yml`
|
||||
|
||||
Adicionar blocos `spring.ai.*` e `app.ai.*` (ver `configuration.md`).
|
||||
|
||||
### 5.3 `job/model/enums/JobType.java`
|
||||
|
||||
Adicionar `GENERATE_EMBEDDINGS`.
|
||||
|
||||
### 5.4 `job/package-info.java`
|
||||
|
||||
Adicionar `ai::services` e `ai::model::entity` aos `allowedDependencies`.
|
||||
|
||||
### 5.5 `job/service/handler/StaticAnalysisHandler.java`
|
||||
|
||||
Adicionar publicação de `StaticAnalysisCompletedEvent` ao final de `process()`.
|
||||
|
||||
### 5.6 `.gitignore`
|
||||
|
||||
Adicionar `.env`.
|
||||
|
||||
## 6. Arquivos Novos (por módulo)
|
||||
|
||||
### Módulo `ai/`
|
||||
|
||||
```
|
||||
src/main/java/ai/decompile/ai/
|
||||
├── package-info.java
|
||||
├── config/
|
||||
│ └── AiConfig.java
|
||||
├── model/
|
||||
│ ├── entity/
|
||||
│ │ ├── package-info.java
|
||||
│ │ ├── EmbeddingChunk.java
|
||||
│ │ ├── ChatSession.java
|
||||
│ │ └── ChatMessage.java
|
||||
│ ├── repository/
|
||||
│ │ ├── package-info.java
|
||||
│ │ ├── EmbeddingChunkRepository.java
|
||||
│ │ ├── ChatSessionRepository.java
|
||||
│ │ └── ChatMessageRepository.java
|
||||
│ └── dto/
|
||||
│ ├── package-info.java
|
||||
│ ├── ChatRequest.java
|
||||
│ ├── SessionResponse.java
|
||||
│ └── MessageResponse.java
|
||||
├── service/
|
||||
│ ├── package-info.java
|
||||
│ ├── EmbeddingService.java
|
||||
│ ├── RagService.java
|
||||
│ └── ChatService.java
|
||||
└── controller/
|
||||
├── package-info.java
|
||||
└── ChatController.java
|
||||
```
|
||||
|
||||
### Módulo `analysis/`
|
||||
|
||||
```
|
||||
src/main/java/ai/decompile/analysis/
|
||||
└── event/
|
||||
└── StaticAnalysisCompletedEvent.java # novo
|
||||
```
|
||||
|
||||
### Módulo `job/`
|
||||
|
||||
```
|
||||
src/main/java/ai/decompile/job/
|
||||
├── service/
|
||||
│ ├── EmbeddingEventSubscriber.java # novo
|
||||
│ └── handler/
|
||||
│ └── EmbeddingGenerationHandler.java # novo
|
||||
└── model/enums/
|
||||
└── JobType.java # modificado (+ GENERATE_EMBEDDINGS)
|
||||
```
|
||||
|
||||
### Database
|
||||
|
||||
```
|
||||
src/main/resources/db/migration/
|
||||
└── V008__ai_embedding_and_chat.sql # novo
|
||||
```
|
||||
|
||||
### Raiz do projeto
|
||||
|
||||
```
|
||||
backend/
|
||||
├── .env # novo
|
||||
├── .env.example # novo (template sem secrets)
|
||||
└── .gitignore # modificado (+ .env)
|
||||
```
|
||||
|
||||
## 7. Ordem de Implementação Recomendada
|
||||
|
||||
| Ordem | Tarefa | Depende de |
|
||||
|---|---|---|
|
||||
| 1 | `V008__ai_embedding_and_chat.sql` | Nada |
|
||||
| 2 | Dependências no `pom.xml` | Nada |
|
||||
| 3 | `application.yml` — blocos AI | Nada |
|
||||
| 4 | `.env` + `.env.example` + `.gitignore` | Nada |
|
||||
| 5 | Entidades JPA (`EmbeddingChunk`, `ChatSession`, `ChatMessage`) | V008, pom.xml |
|
||||
| 6 | Repositórios JPA | Entidades |
|
||||
| 7 | `AiConfig.java` | pom.xml, application.yml |
|
||||
| 8 | `EmbeddingService` | Repositórios, AiConfig |
|
||||
| 9 | `RagService` | EmbeddingService, AnalysisService |
|
||||
| 10 | `ChatService` | RagService, AiConfig |
|
||||
| 11 | `ChatController` | ChatService |
|
||||
| 12 | `StaticAnalysisCompletedEvent` | Nada |
|
||||
| 13 | `EmbeddingEventSubscriber` | Evento, JobService |
|
||||
| 14 | `EmbeddingGenerationHandler` | EmbeddingService |
|
||||
| 15 | `JobType.GENERATE_EMBEDDINGS` | Nada |
|
||||
| 16 | `job/package-info.java` update | Dependências ai |
|
||||
| 17 | `StaticAnalysisHandler` — publish event | Evento |
|
||||
| 18 | `ai/package-info.java` | Dependências workspace, analysis |
|
||||
| 19 | Testes de integração | Tudo acima |
|
||||
|
||||
## 8. Verificação de Integridade
|
||||
|
||||
Após implementar todos os pontos de integração, verificar:
|
||||
|
||||
```bash
|
||||
# 1. Compilação limpa
|
||||
./mvnw clean compile
|
||||
|
||||
# 2. Migration roda sem erros
|
||||
./mvnw flyway:migrate
|
||||
|
||||
# 3. Testes de módulo Modulith
|
||||
./mvnw test -Dtest="*Modulith*"
|
||||
|
||||
# 4. Fluxo completo (manual):
|
||||
# - Upload de binário
|
||||
# - Análise estática
|
||||
# - Verificar job GENERATE_EMBEDDINGS criado e concluído
|
||||
# - Verificar registros em embedding_chunk
|
||||
# - Criar sessão de chat
|
||||
# - Enviar mensagem e receber SSE stream
|
||||
```
|
||||
404
docs/ai/rag-pipeline.md
Normal file
404
docs/ai/rag-pipeline.md
Normal file
@ -0,0 +1,404 @@
|
||||
# Pipeline RAG (Retrieval-Augmented Generation)
|
||||
|
||||
## Visão Geral
|
||||
|
||||
O pipeline RAG do Decompile-AI permite que o usuário faça perguntas em linguagem natural sobre um binário analisado. O sistema recupera automaticamente as funções e informações mais relevantes do binário e as injeta no prompt da LLM, que então gera uma resposta contextualizada.
|
||||
|
||||
O pipeline opera em duas fases distintas:
|
||||
|
||||
1. **Fase de Indexação** — Processa os resultados da análise estática, gera embeddings e os armazena no pgvector
|
||||
2. **Fase de Query** — A cada pergunta do usuário, recupera contexto relevante e gera a resposta via LLM
|
||||
|
||||
---
|
||||
|
||||
## Fase 1: Indexação
|
||||
|
||||
### 1.1 Gatilho
|
||||
|
||||
A indexação é disparada automaticamente quando uma análise estática é concluída com sucesso:
|
||||
|
||||
```
|
||||
StaticAnalysisHandler.process() conclui
|
||||
│
|
||||
└── publica StaticAnalysisCompletedEvent(binaryId, projectId, workspaceId, analysisId, engine)
|
||||
│
|
||||
└── EmbeddingEventSubscriber.on(StaticAnalysisCompletedEvent)
|
||||
│
|
||||
└── cria Job(type=GENERATE_EMBEDDINGS, status=ENQUEUED)
|
||||
│
|
||||
└── RabbitMQ → EmbeddingGenerationHandler.process(job)
|
||||
```
|
||||
|
||||
Também é possível reindexar manualmente (endpoint futuro).
|
||||
|
||||
### 1.2 Coleta de Dados
|
||||
|
||||
O `EmbeddingGenerationHandler` chama `EmbeddingService.indexBinary(binaryId)`. O serviço:
|
||||
|
||||
1. **Deleta embeddings existentes** para o binário (`deleteByBinaryId`) — hard delete, sem soft delete
|
||||
2. **Busca a análise mais recente** via `AnalysisService.getAnalysis(analysisId)` ou `listAnalyses(binaryId)`
|
||||
3. **Itera sobre todas as `StaticFunction`** da análise, carregando:
|
||||
- `name`, `address`, `assembly` (sempre presentes)
|
||||
- `decompiledCode`, `signature` (quando disponíveis — Ghidra/IDA 7)
|
||||
- `StaticLabel` associadas à função
|
||||
- `StaticXref` (callers e callees) — nomes das funções via join
|
||||
4. **Busca metadados do binário**: formato, arquitetura, compilador (tabela `binaries`)
|
||||
|
||||
### 1.3 Chunking
|
||||
|
||||
Cada função gera um chunk de texto que será embeddado. O template do chunk:
|
||||
|
||||
```
|
||||
[FUNCTION] {name} at {address}
|
||||
Binary: {filename} | {architecture} | {compiler} | {format}
|
||||
Callers: {caller1}, {caller2}, ...
|
||||
Callees: {callee1}, {callee2}, ...
|
||||
Labels:
|
||||
{label1_address} ({label1_name})
|
||||
{label2_address} ({label2_name})
|
||||
|
||||
[ASSEMBLY]
|
||||
{assembly_code}
|
||||
```
|
||||
|
||||
**Regras de chunking**:
|
||||
|
||||
| Regra | Descrição |
|
||||
|---|---|
|
||||
| **1 chunk = 1 função** | Unidade semântica natural da análise de binários |
|
||||
| **Truncagem** | Se o texto montado exceder `chunk-max-chars` (default 3000), o assembly é truncado |
|
||||
| **Labels inline** | Labels são incluídos como comentários contextuais para enriquecer o embedding |
|
||||
| **Xrefs como metadados** | Nomes de callers/callees são incluídos como "dicas semânticas" no texto |
|
||||
| **Chunk de metadados** | Um chunk extra (tipo `METADATA`) com informações gerais do binário é sempre gerado |
|
||||
|
||||
**Exemplo de chunk gerado** (para `malware.exe` x86 com MSVC):
|
||||
|
||||
```
|
||||
[FUNCTION] sub_401000 at 0x401000
|
||||
Binary: malware.exe | x86 | MSVC 19.0 | PE32
|
||||
Callers: WinMain, sub_402300
|
||||
Callees: CreateFileA, WriteFile, CloseHandle, sub_401200
|
||||
Labels:
|
||||
0x40100A (check_password)
|
||||
0x401050 (error_msg)
|
||||
|
||||
[ASSEMBLY]
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
sub esp, 0x40
|
||||
push ecx
|
||||
push edx
|
||||
mov eax, [ebp+8]
|
||||
push eax
|
||||
call ds:CreateFileA
|
||||
mov [ebp-4], eax
|
||||
cmp eax, -1
|
||||
jz short loc_401050
|
||||
...
|
||||
```
|
||||
|
||||
**Chunk de metadados** (tipo `METADATA`):
|
||||
|
||||
```
|
||||
[METADATA] Binary Overview
|
||||
Filename: malware.exe
|
||||
Format: PE32 (Portable Executable 32-bit)
|
||||
Architecture: x86 (Intel 32-bit)
|
||||
Compiler: Microsoft Visual C++ 19.0
|
||||
Entry Point: 0x401200
|
||||
Function count: 342
|
||||
Analysis engine: IDA5
|
||||
```
|
||||
|
||||
Esse chunk de metadados responde perguntas como "qual arquitetura?", "com o que foi compilado?", "quantas funções tem?".
|
||||
|
||||
### 1.4 Geração de Embeddings
|
||||
|
||||
Para cada chunk de texto:
|
||||
|
||||
1. O texto é enviado ao **Ollama** via Spring AI `EmbeddingModel.embed(document)`
|
||||
2. O modelo configurado é `qwen3-embedding:latest` (8B parâmetros, dimensão configurável)
|
||||
3. A dimensão padrão dos vetores é **1024** (configurável em `app.ai.embedding.dimension`)
|
||||
4. Os embeddings são gerados em **batch** de até `batch-size` (default 20) chunks por chamada
|
||||
5. Cada embedding é um `float[1024]` (ou dimensão configurada)
|
||||
6. O vetor é persistido na tabela `embedding_chunk` junto com o texto original e metadados
|
||||
|
||||
**Fluxo detalhado do batch**:
|
||||
|
||||
```
|
||||
funções: [f1, f2, f3, ..., f342]
|
||||
│
|
||||
│ particiona em batches de 20
|
||||
▼
|
||||
batch1: [f1..f20] → Ollama embed() → 20 × float[1024] → INSERT batch
|
||||
batch2: [f21..f40] → Ollama embed() → 20 × float[1024] → INSERT batch
|
||||
...
|
||||
batch18: [f341..f342] → Ollama embed() → 2 × float[1024] → INSERT batch
|
||||
│
|
||||
▼
|
||||
chunk METADATA → Ollama embed() → 1 × float[1024] → INSERT
|
||||
```
|
||||
|
||||
**Tratamento de erros**:
|
||||
- Se a API Ollama falhar para um batch, o job é marcado como `FAILED`
|
||||
- O `retry_count` do job é incrementado (política de retry via RabbitMQ dead-letter queue no futuro)
|
||||
- Nenhum embedding parcial é salvo — a transação faz rollback
|
||||
|
||||
### 1.5 Re-indexação
|
||||
|
||||
Quando um binário é re-analisado (ex: trocar de engine IDA5 → Ghidra):
|
||||
|
||||
1. O `EmbeddingEventSubscriber` cria novo job `GENERATE_EMBEDDINGS`
|
||||
2. `EmbeddingService.indexBinary()` chama `deleteByBinaryId()` primeiro → **hard delete** de todos os chunks antigos
|
||||
3. Novos chunks são gerados a partir da análise mais recente
|
||||
|
||||
Isso garante que não há embeddings órfãos ou inconsistentes.
|
||||
|
||||
---
|
||||
|
||||
## Fase 2: Query (Chat)
|
||||
|
||||
### 2.1 Visão Geral do Fluxo
|
||||
|
||||
```
|
||||
Usuário: "Quais funções nesse binário lidam com rede?"
|
||||
│
|
||||
▼
|
||||
ChatController → ChatService.chat(sessionId, message)
|
||||
│
|
||||
├── 1. Busca sessão + histórico recente (últimas N mensagens)
|
||||
│
|
||||
├── 2. ChatService.buildContext(binaryId, query, binary)
|
||||
│ │
|
||||
│ ├── 2a. SemanticSearch: embed query → pgvector <=>
|
||||
│ │ └── top-K chunks por similaridade de cosseno
|
||||
│ │
|
||||
│ ├── 2b. TextualFallback (se semantic < topK * 0.4):
|
||||
│ │ └── Extrai tokens da query → LIKE search no content
|
||||
│ │
|
||||
│ └── 2c. CallGraphExpansion:
|
||||
│ ├── Detecta nomes de função/endereço na query
|
||||
│ ├── Busca funções via static_function (ILIKE)
|
||||
│ └── Expande 1-hop (callers + callees) + assembly inline
|
||||
│
|
||||
├── 3. Merge + dedup: requested functions first, then semantic/textual chunks
|
||||
│
|
||||
├── 4. Build system prompt com contexto RAG + histórico
|
||||
│
|
||||
├── 5. LLM (Gemma4:12b) → streaming
|
||||
│ │
|
||||
│ └── Flux<String> via StreamingChatModel.stream()
|
||||
│
|
||||
├── 6. Salva UserMessage + AssistantMessage no banco
|
||||
│
|
||||
└── 7. Retorna SSE stream para o frontend
|
||||
```
|
||||
|
||||
### 2.2 Busca Semântica (pgvector)
|
||||
|
||||
A busca vetorial é a espinha dorsal do retrieval. Utiliza o operador de distância de cosseno do pgvector:
|
||||
|
||||
```sql
|
||||
SELECT id, content, metadata, token_count,
|
||||
1 - (embedding <=> CAST(:queryEmbedding AS vector)) AS similarity
|
||||
FROM embedding_chunk
|
||||
WHERE binary_id = :binaryId
|
||||
AND 1 - (embedding <=> CAST(:queryEmbedding AS vector)) > :threshold
|
||||
ORDER BY embedding <=> CAST(:queryEmbedding AS vector)
|
||||
LIMIT :limit
|
||||
```
|
||||
|
||||
- **`<=>`** é o operador de cosine distance do pgvector (0 = idêntico, 2 = oposto)
|
||||
- **`1 - (<=>)`** converte para similaridade (1 = idêntico, -1 = oposto)
|
||||
- **`threshold`** (default 0.6) filtra resultados pouco relevantes
|
||||
- **`limit`** (default 10) retorna os top-K chunks
|
||||
- **`binary_id`** restringe a busca ao binário específico (escopo da sessão)
|
||||
|
||||
O índice HNSW (`idx_embedding_chunk_embedding`) acelera a busca para complexidade O(log N).
|
||||
|
||||
**Modelo de embedding da query**:
|
||||
|
||||
A pergunta do usuário é embeddada usando o **mesmo modelo** da indexação (`qwen3-embedding:latest`), garantindo que query e documentos estejam no mesmo espaço vetorial.
|
||||
|
||||
### 2.2.1 Fallback por Busca Textual
|
||||
|
||||
Quando a busca semântica retorna poucos resultados (menos de `fallback-min-ratio * topK`, default `0.4 * 10 = 4`), o sistema complementa com **busca textual direta** via `LIKE` no conteúdo dos chunks:
|
||||
|
||||
```sql
|
||||
SELECT * FROM embedding_chunk
|
||||
WHERE binary_id = :binaryId
|
||||
AND LOWER(content) LIKE LOWER(CONCAT('%', :term, '%'))
|
||||
LIMIT :limit
|
||||
```
|
||||
|
||||
**Estratégia**:
|
||||
1. A query é tokenizada (split por espaços e pontuação)
|
||||
2. Tokens com ≥ 3 caracteres são usados como termos de busca
|
||||
3. Para cada termo, busca textual preenche os slots restantes até `topK`
|
||||
4. Resultados são deduplicados (mesmo chunk pode aparecer em múltiplas buscas)
|
||||
|
||||
**Exemplo**: Query *"Quais funções lidam com rede?"*
|
||||
- Tokens: `Quais`, `funções`, `lidam`, `rede`
|
||||
- Cada token vira `LIKE '%rede%'` no conteúdo dos chunks
|
||||
- Funções que mencionam `WS2_32.send`, `socket`, `connect` são recuperadas mesmo que o embedding semântico não as tenha rankeado bem
|
||||
|
||||
### 2.3 Expansão por Call Graph (Recuperação Estruturada)
|
||||
|
||||
O sistema detecta automaticamente menções a funções ou endereços na query do usuário e recupera o contexto via call graph:
|
||||
|
||||
**Detecção**:
|
||||
1. Tokenização da query + regex para padrões comuns: `sub_XXXX`, `0xXXXXXXXX`, nomes em PascalCase
|
||||
2. Para cada token candidato, busca em `static_function` via:
|
||||
```java
|
||||
List<StaticFunction> findByNameIgnoreCaseContainingAndAnalysisId(name, analysisId);
|
||||
```
|
||||
3. Até 5 funções são identificadas (limite para não poluir o contexto)
|
||||
|
||||
**Expansão 1-hop**:
|
||||
- **Callers**: quem chama a função detectada (`xrefRepository.findByCalleeId`)
|
||||
- **Callees**: quem a função detectada chama (`xrefRepository.findByCallerId`)
|
||||
- Para funções detectadas (que podem não ter embedding), o assembly é montado **inline no contexto**, truncado a 5000 caracteres
|
||||
|
||||
**Prioridade no contexto**:
|
||||
1. Funções explicitamente mencionadas na query + seu assembly (sempre incluídas, mesmo sem embedding)
|
||||
2. Callers/callees imediatos (1-hop, assembly truncado a 3000 caracteres)
|
||||
3. Chunks semânticos (pgvector top-K)
|
||||
4. Chunks textuais (LIKE fallback)
|
||||
|
||||
### 2.4 Construção do System Prompt
|
||||
|
||||
O `ChatService.buildSystemPrompt()` monta o prompt do sistema que instrui a LLM:
|
||||
|
||||
```
|
||||
You are an expert reverse engineer and malware analyst.
|
||||
You are analyzing a binary file with the following characteristics.
|
||||
Use the provided function disassembly and context to answer the user's questions.
|
||||
When referencing code, mention function names, addresses, and the binary context.
|
||||
If the provided context does not contain enough information, acknowledge the limitation
|
||||
and suggest what additional analysis would help.
|
||||
|
||||
[BINARY OVERVIEW]
|
||||
Filename: malware.exe
|
||||
Format: PE32 (Portable Executable 32-bit)
|
||||
Architecture: x86
|
||||
Compiler: Microsoft Visual C++ 19.0
|
||||
|
||||
[REQUESTED FUNCTION ANALYSIS] ← call graph expansion (se houver)
|
||||
|
||||
[FUNCTION] sub_401000 at 0x401000
|
||||
Callers: WinMain, sub_402300
|
||||
Callees: CreateFileA, WriteFile, CloseHandle
|
||||
[ASSEMBLY]
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
...
|
||||
|
||||
[RELEVANT CODE SECTIONS] ← semantic + textual results
|
||||
|
||||
Section 1:
|
||||
[FUNCTION] sub_401200 at 0x401200
|
||||
Callers: sub_401000, sub_402500
|
||||
Callees: WS2_32.send, WS2_32.recv
|
||||
[ASSEMBLY]
|
||||
...
|
||||
|
||||
[CONVERSATION GUIDELINES]
|
||||
- Cite function addresses when relevant (e.g., "the function at 0x401000")
|
||||
- Explain assembly in natural language when useful
|
||||
- If you identify patterns (loops, conditionals, API calls), highlight them
|
||||
- If you detect malicious intent, point it out objectively
|
||||
- Be concise but thorough in your technical analysis
|
||||
- If the context includes a [REQUESTED FUNCTION ANALYSIS] section with full assembly,
|
||||
analyze that function in depth rather than saying you don't have access to it
|
||||
```
|
||||
- Explique o assembly em linguagem natural quando útil
|
||||
- Se identificar padrões (ex: loop, condicional, chamada de API), destaque-os
|
||||
- Se perceber intenção maliciosa, aponte objetivamente
|
||||
```
|
||||
|
||||
### 2.6 Truncagem de Contexto
|
||||
|
||||
O contexto total (system prompt + chunks + histórico) deve caber na janela de contexto da LLM:
|
||||
|
||||
| Modelo | Janela de Contexto | Max Context Tokens Config |
|
||||
|---|---|---|
|
||||
| Gemma4:12b | 256K tokens | 8000 (padrão) |
|
||||
| DeepSeek-chat | 64K tokens | 8000 (padrão) |
|
||||
|
||||
**Algoritmo de truncagem**:
|
||||
|
||||
1. **Reserva para system prompt**: ~500 tokens (fixo)
|
||||
2. **Reserva para histórico**: últimas 20 mensagens, truncadas se necessário (~2000 tokens)
|
||||
3. **Reserva para resposta**: ~1000 tokens (output)
|
||||
4. **Disponível para chunks**: `max-context-tokens - 500 - 2000 - 1000 = ~4500 tokens`
|
||||
5. **Preenche chunks em ordem de relevância** até atingir o limite
|
||||
6. Chunks que não cabem são descartados (os menos relevantes primeiro)
|
||||
|
||||
### 2.7 Histórico de Conversa
|
||||
|
||||
O histórico da sessão (`chat_message`) é incluído no prompt para manter contexto multi-turn:
|
||||
|
||||
```
|
||||
Messages enviados à LLM:
|
||||
[
|
||||
{ role: SYSTEM, content: "<system prompt com RAG context>" },
|
||||
{ role: USER, content: "Quantas funções esse binário tem?" },
|
||||
{ role: ASSISTANT, content: "342 funções." },
|
||||
{ role: USER, content: "E dessas, quantas importam APIs de rede?" },
|
||||
// ↑ A LLM usa o contexto do system prompt + histórico para responder
|
||||
]
|
||||
```
|
||||
|
||||
- Apenas as últimas `max-history-messages` (default 20) são incluídas
|
||||
- Mensagens muito antigas são truncadas para caber no orçamento de tokens
|
||||
|
||||
### 2.8 Resposta Streaming (SSE)
|
||||
|
||||
Tokens são enviados ao frontend assim que gerados pela LLM:
|
||||
|
||||
```
|
||||
data: {"type":"chunk","content":"O binário"}
|
||||
|
||||
data: {"type":"chunk","content":" importa "}
|
||||
|
||||
data: {"type":"chunk","content":"funções de"}
|
||||
|
||||
data: {"type":"chunk","content":" rede via"}
|
||||
|
||||
data: {"type":"chunk","content":" WS2_32"}
|
||||
|
||||
...
|
||||
|
||||
data: {"type":"done","messageId":"550e8400-e29b-...","sessionId":"660e8400-..."}
|
||||
```
|
||||
|
||||
### 2.9 Tipos de Query e Estratégias
|
||||
|
||||
| Categoria | Exemplo de Query | Estratégia de Retrieval |
|
||||
|---|---|---|
|
||||
| **Análise de função** | "O que a função sub_401000 faz?" | Call graph expansion + assembly inline (1-hop) |
|
||||
| **Busca por nome** | "Explique sub_f5" | Textual fallback (LIKE) + call graph se detectado |
|
||||
| **Busca semântica** | "Quais funções lidam com criptografia?" | pgvector similarity search (top-10) |
|
||||
| **Navegação do grafo** | "Quem chama WinMain?" | Call graph (callers diretos via xrefs) |
|
||||
| **Metadados** | "Qual arquitetura/com pilador?" | Chunk METADATA (sempre incluído) |
|
||||
| **Strings/Imports** | "Esse binário usa rede?" | pgvector (chunks que mencionam imports) |
|
||||
| **Classificação** | "Isso parece malware?" | pgvector + METADATA (visão holística) |
|
||||
| **Controle de fluxo** | "Explique o fluxo a partir de main" | Call graph (multi-hop callees) |
|
||||
|
||||
> **Nota**: O fallback textual e a expansão por call graph garantem que queries com nomes específicos de funções (ex: `sub_f5`, `0x401000`) sempre retornem o assembly relevante — mesmo que a busca semântica retorne 0 resultados.
|
||||
|
||||
---
|
||||
|
||||
## Modelo de Embedding: Qwen3 Embedding
|
||||
|
||||
| Propriedade | Valor |
|
||||
|---|---|
|
||||
| Modelo | `qwen3-embedding:latest` (8B) |
|
||||
| Tamanho | 4.7 GB |
|
||||
| Contexto máximo | 32K tokens |
|
||||
| Dimensão configurável | 32–4096 (Spring AI 1.0.1 não expõe o parâmetro; usamos default do modelo: **4096**) |
|
||||
| Ranking MTEB | #1 (score 70.58) |
|
||||
| Idiomas | 100+ (incluindo código) |
|
||||
| Execução | Ollama local (localhost:11434) |
|
||||
|
||||
**Escolha da dimensão 4096**: O modelo `qwen3-embedding:latest` (8B) retorna 4096 dimensões por padrão. O Spring AI 1.0.1 não expõe o parâmetro `dimensions` da API do Ollama (é um campo top-level, não dentro de `options`). Para 330 funções a 4096 floats (4 bytes cada), o storage total de vetores é ~5.4 MB. A coluna é `vector(4096)` e a busca usa sequential scan (sem índice — HNSW e IVFFlat do pgvector limitados a 2000d).
|
||||
292
docs/ida5-extraction-points.md
Normal file
292
docs/ida5-extraction-points.md
Normal file
@ -0,0 +1,292 @@
|
||||
# Pontos de Extração — IDA 5, IDA 7 e Ghidra
|
||||
|
||||
Documento de referência sobre quais dados de análise estática são extraíveis via script de cada engine, com a utilidade de cada ponto e grau de dificuldade.
|
||||
|
||||
---
|
||||
|
||||
## Tabela Resumo
|
||||
|
||||
| # | Dado | Utilidade | IDA 5 (IDC) | IDA 7+ (IDAPython) | Ghidra (Java/Python) |
|
||||
|---|------|-----------|:-----------:|:------------------:|:--------------------:|
|
||||
| 1 | Funções (nome, endereço, assembly) | Navegação básica do binário | ✅ Fácil | ✅ Fácil | ✅ Fácil |
|
||||
| 2 | Code references / Labels | Resolver alvos de jump/call | ✅ Fácil | ✅ Fácil | ✅ Fácil |
|
||||
| 3 | Xrefs de entrada (callers) | Grafo de chamadas reverso | ✅ Fácil | ✅ Fácil | ✅ Fácil |
|
||||
| 4 | Entry point | Saber onde o binário inicia | ✅ Fácil | ✅ Fácil | ✅ Fácil |
|
||||
| 5 | Tipo de xref (CALL vs JUMP vs DATA) | Diferencia saltos de chamadas | ✅ Fácil | ✅ Fácil | ✅ Fácil |
|
||||
| 6 | Flags da função (library, thunk, far) | Classificar tipo de função | ✅ Fácil | ✅ Fácil | ✅ Fácil |
|
||||
| 7 | Segmentos (.text, .data, .rdata) | Mapa de memória do binário | ✅ Fácil | ✅ Fácil | ✅ Fácil |
|
||||
| 8 | Tamanho/fim da função | Delimitar escopo da função | ✅ Fácil | ✅ Fácil | ✅ Fácil |
|
||||
| 9 | Strings referenciadas | Entender mensagens, nomes, paths | ✅ Médio | ✅ Fácil | ✅ Fácil |
|
||||
| 10 | Imports / APIs externas (DLLs) | Saber quais libs externas são usadas | ✅ Médio | ✅ Fácil | ✅ Fácil |
|
||||
| 11 | Frame da função (vars locais, args) | Estrutura da stack frame | ✅ Médio | ✅ Fácil | ✅ Fácil |
|
||||
| 12 | Comentários do IDA/Ghidra | Anotações automáticas do disassembler | ✅ Médio | ✅ Fácil | ✅ Fácil |
|
||||
| 13 | Data xrefs (leitura/escrita em globais) | Quem acessa cada variável global | ⚠️ Limitado | ✅ Médio | ✅ Médio |
|
||||
| 14 | Tipos e assinaturas de funções | Parâmetros, retorno, calling convention | ❌ Não viável | ✅ Médio | ✅ Fácil |
|
||||
| 15 | Estruturas e enums | Layout de structs usadas pelo binário | ❌ Não viável | ⚠️ Limitado | ✅ Médio |
|
||||
| 16 | Decompilação (pseudocódigo) | Código de alto nível reconstruído | ❌ Não existe | ✅ Fácil | ✅ Fácil |
|
||||
| 17 | Call graph / Fluxo de controle (CFG) | Visualização de blocos e branches | ⚠️ Limitado | ✅ Médio | ✅ Médio |
|
||||
| 18 | Nomes de variáveis globais | Labels simbólicos de dados | ✅ Médio | ✅ Fácil | ✅ Fácil |
|
||||
|
||||
---
|
||||
|
||||
## 1. Funções (nome, endereço, assembly)
|
||||
|
||||
**O que é:** Lista de todas as funções reconhecidas pelo disassembler, com nome, endereço de início e o texto do assembly.
|
||||
|
||||
**Para que serve:** É a base de toda navegação. Permite listar funções, buscar por nome, e exibir o código de cada uma.
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | `NextFunction()`, `GetFunctionName()`, `GetDisasm()`, `ItemSize()` | ✅ **Já implementado** |
|
||||
| IDA 7 | `ida_funcs.get_funcs()`, `ida_lines.generate_disasm_line()` | ✅ Trivial |
|
||||
| Ghidra | `currentProgram.getFunctionManager().getFunctions()`, `listing.getInstructions()` | ✅ Trivial |
|
||||
|
||||
---
|
||||
|
||||
## 2. Code References / Labels
|
||||
|
||||
**O que é:** Para cada instrução de jump/call, o endereço de destino e o nome (label) associado a ele.
|
||||
|
||||
**Para que serve:** Permite que o frontend resolva labels (`loc_28D`, `sub_402000`) para endereços reais e ofereça navegação por clique.
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | `Rfirst(ea)`, `Rnext(ea, ref)`, `Name(ref)` | ✅ **Já implementado** |
|
||||
| IDA 7 | `ida_xref.xrefblk_t()` com `iscode=True` | ✅ Trivial |
|
||||
| Ghidra | `instruction.getReferencesFrom()`, `ref.getToAddress()` | ✅ Trivial |
|
||||
|
||||
---
|
||||
|
||||
## 3. Xrefs de entrada (callers)
|
||||
|
||||
**O que é:** Lista de funções que chamam/referenciam a função atual.
|
||||
|
||||
**Para que serve:** Constrói o grafo reverso de chamadas — "quem me chama". Essencial para entender o fluxo do programa.
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | `RfirstB(ea)`, `RnextB(ea, xref)` | ✅ **Já implementado** |
|
||||
| IDA 7 | `ida_xref.xrefs_to(ea)` | ✅ Trivial |
|
||||
| Ghidra | `function.getCallingFunctions(TaskMonitor.DUMMY)` ou via reference manager | ✅ Trivial |
|
||||
|
||||
---
|
||||
|
||||
## 4. Entry Point
|
||||
|
||||
**O que é:** O endereço onde a execução do binário começa. Pode ser o stub da CRT (`start`) ou diretamente `main`/`WinMain`.
|
||||
|
||||
**Para que serve:** Ponto de partida da análise. O analista sabe por onde começar a ler o código. Em modo headless do frontend, pode ser usado como âncora inicial.
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | `BeginEA()` retorna o entry point; `Name()` dá o label | ✅ Trivial (testado, funciona) |
|
||||
| IDA 7 | `ida_loader.get_entry_point()` ou `ida_entry.get_entry_ordinals()` | ✅ Trivial |
|
||||
| Ghidra | `program.getListing().getInstructionAt(program.getImageBase().add(entryOffset))`; entry offset via `program.getExecutableFormat()` ou metadados do importador | ✅ Trivial |
|
||||
|
||||
---
|
||||
|
||||
## 5. Tipo de Xref (CALL vs JUMP vs DATA)
|
||||
|
||||
**O que é:** Classifica cada code reference como `CALL` (chamada de função), `JUMP` (salto condicional/incondicional) ou referência a dados.
|
||||
|
||||
**Para que serve:** Permite que o frontend diferencie visualmente chamadas de função de saltos locais. Essencial para coloração de syntax e navegação contextual.
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | `XrefType()` retorna `fl_CF` (far call), `fl_CN` (near call), `fl_JF` (far jump), `fl_JN` (near jump) | ✅ Fácil (1 enum switch) |
|
||||
| IDA 7 | `ida_xref.xrefblk_t().type` retorna `fl_CF/fl_CN/fl_JF/fl_JN` | ✅ Fácil |
|
||||
| Ghidra | `ref.getReferenceType()` retorna `CALL`, `JUMP`, `DATA`, etc. | ✅ Fácil |
|
||||
|
||||
---
|
||||
|
||||
## 6. Flags da Função
|
||||
|
||||
**O que é:** Atributos da função como: é library function? é thunk? usa far calling convention? tem frame pointer?
|
||||
|
||||
**Para que serve:** Classificação automática de funções. Ex: marcar funções importadas de DLL como "library", diferenciar funções far/near em binários 16-bit, etc.
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | `GetFunctionFlags(ea)` retorna bits: `FUNC_LIB`, `FUNC_THUNK`, `FUNC_FAR`, `FUNC_FRAME`, etc. | ✅ Fácil (bitmask) |
|
||||
| IDA 7 | `ida_funcs.get_func(ea).flags` | ✅ Fácil |
|
||||
| Ghidra | `function.isThunk()`, `function.isLibrary()`, `function.getCallingConvention()` | ✅ Fácil |
|
||||
|
||||
---
|
||||
|
||||
## 7. Segmentos (.text, .data, .rdata, etc.)
|
||||
|
||||
**O que é:** Mapa de memória do binário — cada segmento com nome, endereço inicial, endereço final e classe (CODE, DATA, BSS, etc.).
|
||||
|
||||
**Para que serve:** Contexto para o frontend mostrar em qual região de memória cada função/string está. Permite filtrar funções por segmento (ex: "só .text").
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | `SegByName()`, `SegStart()`, `SegEnd()`, `SegName()`, iteração com `FirstSeg()`/`NextSeg()` | ✅ Fácil (loop de segmentos) |
|
||||
| IDA 7 | `ida_segment.get_segm_list()`, `ida_segment.get_segm_by_name()` | ✅ Fácil |
|
||||
| Ghidra | `program.getMemory().getBlocks()` | ✅ Fácil |
|
||||
|
||||
---
|
||||
|
||||
## 8. Tamanho / Fim da Função
|
||||
|
||||
**O que é:** Endereço final da função (`FindFuncEnd`), permitindo calcular tamanho em bytes e delimitar o escopo.
|
||||
|
||||
**Para que serve:** Já usado internamente no extract.idc. Expor no JSON permite ao frontend mostrar "tamanho: 245 bytes" ou destacar o range da função no mapa de memória.
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | `FindFuncEnd(ea)` | ✅ **Já usado internamente** |
|
||||
| IDA 7 | `ida_funcs.get_func(ea).end_ea` | ✅ Trivial |
|
||||
| Ghidra | `function.getBody().getMaxAddress()` | ✅ Trivial |
|
||||
|
||||
---
|
||||
|
||||
## 9. Strings
|
||||
|
||||
**O que é:** Todas as strings contidas no binário (mensagens, nomes de arquivo, paths, nomes de classes, etc.) com endereço, conteúdo e tipo (ASCII, Unicode).
|
||||
|
||||
**Para que serve:** Essencial para entender o que o programa faz. Strings geralmente revelam funcionalidades, mensagens de erro, comandos, paths de arquivo. O frontend pode oferecer busca e filtro por strings.
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | Iterar sobre segmentos de dados com `isLoaded(ea)` e `GetString(ea, -1, ASCSTR_C)`. Também é possível buscar "string literals" no segmento `.data`/`.rdata` com `GetStringType()`. | ✅ Médio (iteração manual sobre bytes) |
|
||||
| IDA 7 | `ida_bytes.get_strlit_contents()`, `ida_strlist.string_info_t()` — API nativa de strings | ✅ Fácil |
|
||||
| Ghidra | `program.getListing().getDefinedData(true)` filtrado por `StringDataInstance` | ✅ Fácil |
|
||||
|
||||
---
|
||||
|
||||
## 10. Imports / APIs Externas (DLLs)
|
||||
|
||||
**O que é:** Lista de funções externas importadas de DLLs (ex: `KERNEL32.CreateFileA`, `USER32.MessageBoxA`) com nome da DLL, nome da função e endereço na IAT.
|
||||
|
||||
**Para que serve:** Saber quais APIs do sistema operacional o binário usa. Permite inferir capacidades (ex: se importa `WS2_32`, usa rede; se importa `CRYPT32`, usa criptografia).
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | Iterar segmentos externos (`SegByName(".idata")`) e usar `Name(ea)` nos endereços da IAT. Alternativa: parse da tabela de imports manualmente. | ✅ Médio (requer lógica de parse) |
|
||||
| IDA 7 | `ida_nalt.get_import_module_name()`, `idaapi.enum_import_names()` | ✅ Fácil |
|
||||
| Ghidra | `program.getExternalManager().getExternalLibraryNames()`, `getExternalLibraryFunctions()` | ✅ Fácil |
|
||||
|
||||
---
|
||||
|
||||
## 11. Frame da Função (variáveis locais, argumentos)
|
||||
|
||||
**O que é:** Estrutura da stack frame: variáveis locais (com nome e offset negativo do bp), argumentos (offset positivo), saved registers, tamanho total do frame.
|
||||
|
||||
**Para que serve:** Entender a assinatura implícita da função (quantos args, quantas vars locais). O frontend pode exibir uma tabela de variáveis locais junto com o assembly.
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | `GetFrame(ea)` retorna o id da estrutura, `GetStrucSize(id)`, iterar membros com `GetStrucMember()`/`GetMemberName()` | ✅ Médio (API de estruturas frágil no IDA 5) |
|
||||
| IDA 7 | `ida_frame.get_frame(ea)`, `ida_struct.get_struc_members()` | ✅ Fácil |
|
||||
| Ghidra | `function.getStackFrame()`, `stackFrame.getStackVariables()` | ✅ Fácil |
|
||||
|
||||
---
|
||||
|
||||
## 12. Comentários do Disassembler
|
||||
|
||||
**O que é:** Comentários gerados automaticamente pelo disassembler (ex: `; CODE XREF: sub_1000+5A`, `; char` em loads de strings) e comentários manuais do analista.
|
||||
|
||||
**Para que serve:** Enriquece a exibição do assembly. Comentários automáticos do IDA/Ghidra dão dicas de tipos e cross-references que ajudam na leitura.
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | `GetCommentEx(ea, 0)` para comentários regulares, `GetRptCmt(ea)` para comentários repetíveis | ✅ Médio (precisa testar quais flags funcionam) |
|
||||
| IDA 7 | `ida_bytes.get_cmt(ea, False)`, `ida_bytes.get_cmt(ea, True)` | ✅ Fácil |
|
||||
| Ghidra | `listing.getComment(CodeUnit.EOL_COMMENT, address)` e `PRE_COMMENT`, `POST_COMMENT` | ✅ Fácil |
|
||||
|
||||
---
|
||||
|
||||
## 13. Data Xrefs (leitura/escrita em globais)
|
||||
|
||||
**O que é:** Referências de instruções para endereços de dados — quem lê e quem escreve cada variável global.
|
||||
|
||||
**Para que serve:** Entender o fluxo de dados. Ex: "a variável global `g_score` é escrita por `addPoints()` e lida por `drawHUD()`".
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | `Dfirst(ea)` e `Dnext(ea, ref)` retornam data xrefs. API existe mas suporte pode ser limitado. | ⚠️ Limitado (testar viabilidade) |
|
||||
| IDA 7 | `ida_xref.xrefs_to(ea)` com filtro `data`, ou `ida_xref.xrefblk_t()` | ✅ Médio |
|
||||
| Ghidra | `referenceManager.getReferencesTo(address)` e filtrar por tipo `DATA` | ✅ Médio |
|
||||
|
||||
---
|
||||
|
||||
## 14. Tipos e Assinaturas de Funções
|
||||
|
||||
**O que é:** Tipo de retorno, parâmetros com nome e tipo, calling convention (`__cdecl`, `__stdcall`, `__fastcall`), tipo `void*`, tamanho de cada parâmetro.
|
||||
|
||||
**Para que serve:** Assinatura completa da função. Permite ao frontend mostrar "`int sub_1000(HWND hWnd, LPCSTR lpText)`" em vez de só o nome.
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | Suporte a tipos muito limitado. `GetType()` pode retornar string, mas raramente populado. | ❌ **Não viável** |
|
||||
| IDA 7 | `ida_typeinf.get_tinfo(ea)`, `ida_typeinf.print_tinfo()` | ✅ Médio |
|
||||
| Ghidra | `function.getReturnType()`, `function.getParameters()`, `function.getSignature()` | ✅ Fácil |
|
||||
|
||||
---
|
||||
|
||||
## 15. Estruturas e Enums
|
||||
|
||||
**O que é:** Layout de structs e enums reconhecidos pelo disassembler, com nomes de campos e tipos.
|
||||
|
||||
**Para que serve:** Entender estruturas de dados usadas pelo binário. Ex: mostrar que `[ebp+8]` acessa `struct Player.name`.
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | `GetStrucId()`, `GetStrucName()` — API existe mas tipos raramente aplicados pelo IDA 5 | ❌ **Não viável** |
|
||||
| IDA 7 | `ida_struct.get_struc_list()`, `ida_typeinf` para tipos locais | ⚠️ Limitado (depende da análise do IDA) |
|
||||
| Ghidra | `program.getDataTypeManager().getAllStructures()` | ✅ Médio |
|
||||
|
||||
---
|
||||
|
||||
## 16. Decompilação (Pseudocódigo)
|
||||
|
||||
**O que é:** Código de alto nível reconstruído a partir do assembly — `if/else`, `while`, chamadas de função com nomes de parâmetros.
|
||||
|
||||
**Para que serve:** Leitura muito mais rápida do que assembly. É o principal diferencial do Ghidra e IDA 7+ sobre o IDA 5.
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | Não tem decompiler (Hex-Rays só a partir do IDA 6.6+) | ❌ **Não existe** |
|
||||
| IDA 7 | `ida_hexrays.decompile(ea)` retorna AST, `cfunc.print()` gera string | ✅ Fácil (requer licença Hex-Rays) |
|
||||
| Ghidra | `decompInterface.decompileFunction(function, timeout, monitor)` → `decompResults.getDecompiledFunction().getC()` | ✅ Fácil (nativo e gratuito) |
|
||||
|
||||
---
|
||||
|
||||
## 17. Call Graph / Control Flow Graph (CFG)
|
||||
|
||||
**O que é:** Grafo direcionado de chamadas entre funções (call graph) e blocos básicos dentro de cada função (CFG) com arestas representando branches condicionais.
|
||||
|
||||
**Para que serve:** Visualização gráfica do fluxo do programa. O frontend pode renderizar um grafo interativo de chamadas e blocos.
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | `GetFchunkAttr(ea, FUNCATTR_START)` e iteração sobre chunks. `FlowChart` não disponível no IDC 5. | ⚠️ Limitado (possível via chunks) |
|
||||
| IDA 7 | `ida_gdl.FlowChart(f)` retorna nós e arestas; `ida_gdl.gen_flow_graph()` | ✅ Médio |
|
||||
| Ghidra | `BasicBlockModel` + iteração sobre `function.getBody()` → blocos, `function.getCalledFunctions()` → call graph | ✅ Médio |
|
||||
|
||||
---
|
||||
|
||||
## 18. Nomes de Variáveis Globais
|
||||
|
||||
**O que é:** Labels/names atribuídos a endereços de dados globais (ex: `g_score`, `aHelloWorld`).
|
||||
|
||||
**Para que serve:** Dar significado a acessos de memória. Transforma `mov eax, dword_403000` em `mov eax, g_score`.
|
||||
|
||||
| Engine | API | Viabilidade |
|
||||
|--------|-----|:-----------:|
|
||||
| IDA 5 | Iterar segmentos de dados e usar `Name(ea)` + `isData(GetFlags(ea))` | ✅ Médio |
|
||||
| IDA 7 | `ida_name.get_name_list()`, `ida_bytes.is_data()` | ✅ Fácil |
|
||||
| Ghidra | `program.getSymbolTable().getGlobalSymbols()`, `listing.getDataAt()` | ✅ Fácil |
|
||||
|
||||
---
|
||||
|
||||
## Prioridades Recomendadas
|
||||
|
||||
1. **Entry point + Tipo de xref + Flags** — baixo esforço, alto valor, fecha o básico
|
||||
2. **Strings + Imports** — esforço médio, altíssimo valor para entendimento do binário
|
||||
3. **Segmentos + Tamanho da função** — baixo esforço, enriquece o contexto
|
||||
4. **Decompilação** (Ghidra/IDA 7) — esforço médio, é o grande diferencial vs IDA 5
|
||||
5. **Frame + Variáveis globais** — enriquece a visualização de cada função
|
||||
6. **Call graph / CFG** — habilita visualização gráfica no frontend
|
||||
7. **Tipos + Estruturas** — depende muito da engine, menor prioridade
|
||||
Loading…
x
Reference in New Issue
Block a user