Tales-Cunha commited on
Commit
4e2e30e
·
1 Parent(s): c01b9cd

docs: create tester architecture definition

Browse files

- Created Definicao_Agente_Tester.md mapping the 4 phases of the Tester
(Oracle, Analyze, Generate, Run) mimicking the Auditor's PDF structure.
- Also includes the previous uncommitted production gap fixes (mapFinding context pass, customSandboxDir, reduced max iterations).

AGENT_PROGRESS.md CHANGED
@@ -236,3 +236,128 @@ Docker image needs rebuild after code changes.
236
  | Overall Ground Truth | 0.0% | ~20% |
237
  | Avg Iterations | 7.55 | <7.0 |
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  | Overall Ground Truth | 0.0% | ~20% |
237
  | Avg Iterations | 7.55 | <7.0 |
238
 
239
+
240
+ ---
241
+
242
+ ## Production vs Benchmark Gap Analysis
243
+
244
+ ### What the production pipeline actually sends to the tester
245
+
246
+ ```
247
+ User request
248
+
249
+ ▼ Coder Agent
250
+ coderResult.contract ← a single Solidity string (the generated contract)
251
+
252
+ ▼ Auditor Agent (receives repoPath with Contract.sol + README.md)
253
+ auditorResult.findings[0] ← ONE finding
254
+
255
+ ▼ mapFindingToReport() ← the ONLY bridge between auditor and tester
256
+ ```
257
+
258
+ **`mapFindingToReport` currently passes to the tester:**
259
+ | Field | Source | Used by tester? |
260
+ |---|---|---|
261
+ | `id` | derived from title | ✅ identifier only |
262
+ | `severity` | `finding.severity` | label only |
263
+ | `type` | `finding.type` | ✅ in analysis prompt |
264
+ | `title` | `finding.title` | ✅ in analysis prompt |
265
+ | `description` | `finding.description` | ✅ in analysis prompt |
266
+ | `affectedContract.sourceCode` | `coderResult.contract` | ✅ shown to LLM |
267
+ | `affectedContract.name` | extracted from path | ✅ |
268
+ | `attackVector` | `exploitablePaths[0]` | ✅ in analysis prompt |
269
+ | `exploitablePaths` | `judgeReview.exploitablePaths` | ✅ passed |
270
+ | `codeSnippet` | `finding.codeSnippet` | ✅ passed |
271
+ | `location` | `finding.location` | ✅ passed |
272
+
273
+ **What the auditor produces but the tester NEVER receives:**
274
+ | Auditor field | Value | Why it would help the tester |
275
+ |---|---|---|
276
+ | `finding.recommendation` | Human-readable fix suggestion | Tells tester what the PATCH would look like → key for specific assertions |
277
+ | `finding.judgeReview.review` | Judge's analysis of exploitability | More precise attack reasoning than just description |
278
+ | `finding.judgeReview.confidence` | 0-100 confidence score | Tester could skip low-confidence findings |
279
+ | `auditorResult.repoContext` | Full structured protocol context | Gives tester knowledge of cross-contract interactions |
280
+ | `auditorResult.fileTree` | Directory tree of the repo | Helps tester find the right imports |
281
+ | All other `.sol` files | Source of ALL contracts | Tester only gets ONE contract; misses dependencies |
282
+
283
+ **Missing in production but present in benchmark:**
284
+ | Field | Benchmark | Production |
285
+ |---|---|---|
286
+ | `customSandboxDir` | ✅ real project folder | ❌ NOT SET (uses generic /tmp/poc-sandbox) |
287
+ | `referenceTestCode` | ✅ real test files | ❌ NOT SET |
288
+ | `patchDiff` | ✅ computed from dataset | ❌ N/A (no patch in production) |
289
+
290
+ **The critical gap:** In production, `customSandboxDir` is null/undefined, so:
291
+ - BFS test file selection → SKIPPED
292
+ - projectContextExtractor → SKIPPED
293
+ - dependencyStubber → SKIPPED
294
+ - Test file cleanup → SKIPPED
295
+ - The tester runs in the generic `/tmp/poc-sandbox` with NO project context
296
+
297
+ All the improvements that boosted benchmark from 27% → 54% are benchmark-only.
298
+
299
+ ---
300
+
301
+ ## Model vs Agent Quality Plateau
302
+
303
+ **How to tell them apart:**
304
+
305
+ | Symptom | Model limit | Agent limit |
306
+ |---|---|---|
307
+ | Correct assertion but setup wrong | | ✅ Agent can fix |
308
+ | Wrong assertion (generic, too broad) | ✅ Model limit | Could improve with better prompting |
309
+ | Compile errors even with MINIMAL_INTERFACE | | ✅ Agent can fix |
310
+ | Passes vulnerable but also passes patched | ✅ Model semantics | Partially agent (patch context) |
311
+ | Correct overall but random failures across runs | ✅ Temperature/stochastic | |
312
+
313
+ **Current evidence points:**
314
+ - gemini-3.1-flash-lite is a very small/cheap model → likely hitting model ceiling for complex semantic reasoning
315
+ - The compile errors are 100% agent-fixable (and we mostly did)
316
+ - Specificity failures: partly agent bug (patch not applied) + partly model (generic assertions)
317
+
318
+ **Test: try 3 cases with a stronger model to see the ceiling:**
319
+ ```bash
320
+ OPENROUTER_MODEL="google/gemini-2.0-flash" BENCHMARK_LIMIT=3 npx tsx src/benchmark/runTesterBenchmark.ts
321
+ ```
322
+ If specificity jumps to >50% with the stronger model, it's the model. If not, it's the agent prompting.
323
+
324
+ ---
325
+
326
+ ## Next Actions (Priority Order)
327
+
328
+ ### 1. Fix production gap — `mapFindingToReport` (HIGH VALUE, LOW EFFORT)
329
+ Add the missing rich context from the auditor to what the tester receives:
330
+
331
+ ```typescript
332
+ export function mapFindingToReport(finding: any, sourceCode: string,
333
+ repoContext?: string): VulnerabilityReport {
334
+ return {
335
+ // ... existing fields
336
+ description: [
337
+ finding.description,
338
+ finding.recommendation ? `\nFix recommendation: ${finding.recommendation}` : "",
339
+ finding.judgeReview?.review ? `\nJudge analysis: ${finding.judgeReview.review}` : "",
340
+ repoContext ? `\nProtocol context: ${repoContext.slice(0, 1000)}` : "",
341
+ ].filter(Boolean).join("\n"),
342
+ // The recommendation tells the tester what should NOT work after the fix
343
+ // which is the key for writing a specific assertion
344
+ };
345
+ }
346
+ ```
347
+
348
+ ### 2. Pass `repoPath` as `customSandboxDir` in production
349
+ In server.ts, pass `outputDir` (where Contract.sol was written) as `customSandboxDir`:
350
+ ```typescript
351
+ const report = mapFindingToReport(auditorResult.findings[0], coderResult.contract,
352
+ auditorResult.repoContext);
353
+ report.customSandboxDir = outputDir; // ← enables all context improvements
354
+ ```
355
+ Since production uses a single Contract.sol with no remappings or test files, the context
356
+ extractor will find no remappings (graceful fallback), and the dep stubber will run a
357
+ probe but find nothing missing (also fine).
358
+
359
+ ### 3. Lower MAX_ITERATIONS from 10 to 6
360
+ Credits are limited. 10 iterations is too many for flash-lite which repeats itself after ~5.
361
+
362
+ ### 4. Run full benchmark to measure Run 4
363
+ After the test file cleanup fix + patch fix are confirmed working.
Definicao_Agente_Tester.md ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agente Gerador de PoCs (Tester) - Definição do Projeto
2
+
3
+ Este documento detalha a arquitetura e o fluxo de funcionamento do **Agente Gerador de PoCs (Tester)**, seguindo a mesma estrutura de definição utilizada para o Agente Auditor.
4
+
5
+ ---
6
+
7
+ ## Diagrama do Tester
8
+
9
+ O Tester é implementado como um `StateGraph` (LangGraph) que recebe o relatório de vulnerabilidade e itera para gerar, compilar e validar um teste executável (Proof of Concept) usando o framework Foundry.
10
+
11
+ ```mermaid
12
+ graph TD
13
+ A[Vulnerability Report] -->|Entrada| B(Fase 1: Preparação do Oráculo)
14
+ B --> C(Fase 2: Análise da Vulnerabilidade)
15
+ C --> D(Fase 3: Geração do PoC)
16
+ D --> E(Fase 4: Execução no Foundry)
17
+
18
+ E -->|Sucesso| F[Fim - PoC Validado]
19
+ E -->|Falha| G{Iterações < Max?}
20
+
21
+ G -->|Sim| H(Reflection - Analisar Erro)
22
+ H --> D
23
+ G -->|Não| I[Fim - Falha Timeout]
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Fase 1: Preparação do Oráculo (`oracleNode`)
29
+
30
+ O objetivo desta fase é preparar todo o contexto do repositório para garantir que o LLM tenha as informações corretas de importação e de estado antes de gerar código.
31
+
32
+ 1. **Geração do Scaffold:** Cria a estrutura inicial do arquivo de teste (`Exploit.t.sol`), incluindo a função `setUp()` baseada no construtor do contrato alvo.
33
+ 2. **Extração de Contexto do Projeto:**
34
+ - Lê o `remappings.txt` do projeto para resolver caminhos de bibliotecas (ex: `@openzeppelin/`).
35
+ - Usa BFS (Breadth-First Search) para encontrar o arquivo `.t.sol` existente mais complexo e extrair seus `imports` como referência de padrão.
36
+ 3. **Isolamento e Limpeza:** Remove testes nativos do projeto do diretório `test/` do Foundry para evitar conflitos de compilação quando bibliotecas não estão presentes.
37
+ 4. **Criação de Stubs (Dependências Faltantes):** Executa um `forge build` rápido. Se pacotes externos (`node_modules`, `lib/caviar`) estiverem ausentes no ambiente, o agente gera contratos falsos (Stubs) com as interfaces mínimas necessárias para que a compilação prossiga.
38
+ 5. **Análise de API (AST):** Gera a assinatura completa (funções, eventos, erros) do contrato alvo para guiar o LLM.
39
+
40
+ ---
41
+
42
+ ## Fase 2: Análise da Vulnerabilidade (`analyzeVulnerabilityNode`)
43
+
44
+ Esta fase interpreta o relatório do Auditor (e o código fonte) para traçar uma estratégia de ataque antes de escrever o teste.
45
+
46
+ 1. **Recebe o Contexto:** Lê o `VulnerabilityReport` contendo:
47
+ - Descrição da falha
48
+ - Revisão do Juiz (explicação técnica do Auditor)
49
+ - *Exploitable Paths* (passo-a-passo sugerido pelo Auditor)
50
+ - Contexto geral do protocolo
51
+ - Diferença de código da correção (*Patch Diff*, se executado via benchmark).
52
+ 2. **Definição da Estratégia (LLM):** Pede ao LLM para responder 4 perguntas críticas:
53
+ - Qual a causa raiz?
54
+ - Quais as condições de ativação (precondições)?
55
+ - Qual a sequência exata de chamadas para o exploit?
56
+ - **Qual asserção (`assert`) provará a vulnerabilidade?** (Ex: *o saldo roubado deve ser maior que 0*, ou *a chamada deve reverter com X*).
57
+
58
+ ---
59
+
60
+ ## Fase 3: Geração do PoC (`generatePoCNode`)
61
+
62
+ É aqui que o código Solidity do exploit é efetivamente escrito. Esta fase adapta o prompt dependendo do estado atual do loop de reflexão.
63
+
64
+ 1. **Modo `INITIAL`:** (Primeira tentativa) Gera o código usando o Scaffold do oráculo e a estratégia traçada.
65
+ 2. **Modo `FIX_COMPILE`:** (Se falhou ao compilar) Recebe o erro exato do compilador (linha e arquivo). É instruído a verificar imports e tipos.
66
+ 3. **Modo `MINIMAL_INTERFACE`:** (Escape Hatch) **Se a compilação falhar 3 vezes seguidas**, o agente abandona os imports de repositório e injeta interfaces cruas (ex: `interface IERC20 { ... }`) direto no arquivo de teste. Isso salva execuções que falhariam por dependências quebradas.
67
+ 4. **Modo `FIX_LOGIC`:** (Se compilou, mas o teste reverteu) É instruído a verificar a ordem das chamadas, permissões (`vm.prank`), saldo de setup (`vm.deal`) e analisar os *traces* do EVM. Contém padrões prontos para erros clássicos (ex: *Reentrancy callback*, *Unchecked return values*).
68
+
69
+ ---
70
+
71
+ ## Fase 4: Execução no Foundry (`runFoundryNode` & `reflectNode`)
72
+
73
+ 1. **Sanitização do Código:** Extrai o código Solidity do output do LLM. Valida se o contrato se chama `ExploitTest` e a função principal é `test_Exploit()`.
74
+ 2. **Execução Isolada:** Roda o teste dentro de um container/diretório temporário (`customSandboxDir`) usando o comando `forge test`.
75
+ 3. **Análise de Logs (`logAnalyzer`):**
76
+ - Parseia a saída bruta do Forge.
77
+ - Categoriza o erro em: `COMPILER_ERROR`, `ASSERTION_FAILED`, `REVERT_NO_MESSAGE`, `SETUP_FAILED`, etc.
78
+ - Extrai as linhas cruciais do erro (ex: `Error (6275): Source "src/Token.sol" not found`).
79
+ 4. **Decisão:**
80
+ - **Passou:** Retorna sucesso e o código final.
81
+ - **Falhou:** Envia o log sumarizado de volta para a Fase 3 (via `reflectNode`) iterando até o `MAX_ITERATIONS` (atualmente 6).
82
+
83
+ ---
84
+
85
+ ## Resultados (Avaliação do Benchmark PoCo)
86
+
87
+ Para avaliar a resiliência do agente e comprovar a arquitetura, ele foi testado contra o dataset público **ASSERT-KTH/Proof-of-Patch** (22 vulnerabilidades reais auditadas, com correções validadas).
88
+
89
+ **Métricas:**
90
+ * **Reproducibility Rate (Taxa de Reprodução):** Capacidade do agente compilar e rodar um PoC que passe na versão vulnerável.
91
+ * **Specificity Rate (Taxa de Especificidade):** Garantia de que o teste criado *falha* quando executado contra o código já corrigido (prova de que a asserção mirou no bug real, e não em falsos positivos genéricos).
92
+
93
+ **Progressão de Resultados:**
94
+ 1. **Baseline (Sem Oráculo e sem Reflection inteligente):** ~27% de reprodução (falhas massivas de compilação por imports errados).
95
+ 2. **Com Oráculo (Remappings + BFS + Stubs):** As falhas de compilação caíram drasticamente.
96
+ 3. **Com `MINIMAL_INTERFACE` e Prompts Refinados:** Atingiu **54.5%** de reprodução em projetos do mundo real, comprovando a eficácia da adaptação dinâmica do agente perante falhas repetidas.
src/agents/tester/agent.ts CHANGED
@@ -1,4 +1,7 @@
1
  import "dotenv/config";
 
 
 
2
  import { StateGraph, END, START } from "@langchain/langgraph";
3
  import { PoCStateAnnotation, PoCState } from "./state.js";
4
  import { generateLocalScaffold } from "./tools/scaffoldGenerator.js";
@@ -20,7 +23,7 @@ import { createMissingDependencyStubs } from "./utils/dependencyStubber.js";
20
  import { analyzeSolidityFile } from "../auditor/tools/solidity-analyzer-tool.js";
21
  import { extractConstructor } from "./utils/parserUtils.js";
22
 
23
- const MAX_ITERATIONS = 10;
24
 
25
  const llm = createLLM();
26
 
@@ -41,7 +44,6 @@ async function oracleNode(state: PoCState): Promise<Partial<PoCState>> {
41
  referenceTestHelpers = await analyzeSolidityFile(state.report.referenceTestCode, "short");
42
  }
43
 
44
- // STEP 4: Extract project-level context (remappings, existing test imports)
45
  let projectRemappings = "";
46
  let projectTestImports = "";
47
  let projectTestFilePath: string | null = null;
@@ -58,8 +60,28 @@ async function oracleNode(state: PoCState): Promise<Partial<PoCState>> {
58
  console.warn("[oracleNode] could not extract project context:", (e as Error).message);
59
  }
60
 
61
- // STEP 5: Pre-flight dependency stub creation
62
- // Run a quick forge build probe to detect missing dependencies, then stub them
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  try {
64
  await createMissingDependencyStubs(state.report.customSandboxDir);
65
  } catch (e) {
@@ -67,6 +89,7 @@ async function oracleNode(state: PoCState): Promise<Partial<PoCState>> {
67
  }
68
  }
69
 
 
70
  const oracleContext: OracleContext = {
71
  solidityScaffold,
72
  constructorInfo: constructorInfo?.parameters,
 
1
  import "dotenv/config";
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+
5
  import { StateGraph, END, START } from "@langchain/langgraph";
6
  import { PoCStateAnnotation, PoCState } from "./state.js";
7
  import { generateLocalScaffold } from "./tools/scaffoldGenerator.js";
 
23
  import { analyzeSolidityFile } from "../auditor/tools/solidity-analyzer-tool.js";
24
  import { extractConstructor } from "./utils/parserUtils.js";
25
 
26
+ const MAX_ITERATIONS = 6;
27
 
28
  const llm = createLLM();
29
 
 
44
  referenceTestHelpers = await analyzeSolidityFile(state.report.referenceTestCode, "short");
45
  }
46
 
 
47
  let projectRemappings = "";
48
  let projectTestImports = "";
49
  let projectTestFilePath: string | null = null;
 
60
  console.warn("[oracleNode] could not extract project context:", (e as Error).message);
61
  }
62
 
63
+ // STEP 5: Remove existing project test files from sandbox test/ directory.
64
+ // Forge compiles ALL .t.sol files even when only running Exploit.t.sol.
65
+ // Existing tests often import missing deps (@prb/test, lib/caviar, etc.)
66
+ // causing compilation failures even when our Exploit.t.sol is clean.
67
+ // We already extracted the import context we needed — now clean up.
68
+ try {
69
+ const testDir = path.join(state.report.customSandboxDir, "test");
70
+ const testEntries = await fs.readdir(testDir, { withFileTypes: true }).catch(() => []);
71
+ let removed = 0;
72
+ for (const entry of testEntries) {
73
+ if (entry.isFile() && entry.name.endsWith(".t.sol") && entry.name !== "Exploit.t.sol") {
74
+ await fs.unlink(path.join(testDir, entry.name));
75
+ removed++;
76
+ }
77
+ }
78
+ if (removed > 0) console.log(`[oracleNode] Removed ${removed} existing test files from sandbox (avoids missing dep conflicts)`);
79
+ } catch (e) {
80
+ console.warn("[oracleNode] test cleanup failed (non-fatal):", (e as Error).message);
81
+ }
82
+
83
+ // STEP 6: Pre-flight dependency stub creation
84
+ // After removing conflicting test files, create stubs for any remaining missing deps
85
  try {
86
  await createMissingDependencyStubs(state.report.customSandboxDir);
87
  } catch (e) {
 
89
  }
90
  }
91
 
92
+
93
  const oracleContext: OracleContext = {
94
  solidityScaffold,
95
  constructorInfo: constructorInfo?.parameters,
src/server.ts CHANGED
@@ -80,9 +80,15 @@ app.post("/api/run", (c) => {
80
  await send("log", "[Tester] Gerando testes de prova de conceito...");
81
 
82
  if (auditorResult.findings.length > 0) {
83
- const report = mapFindingToReport(auditorResult.findings[0], coderResult.contract);
 
 
 
 
 
84
  const testerResult = await testerAgent.invoke({ report });
85
 
 
86
  await send("log", `[Tester] Execução concluída com status: ${testerResult.status}`);
87
 
88
  // Garante que o objeto enviado tem exatamente o que o front espera
 
80
  await send("log", "[Tester] Gerando testes de prova de conceito...");
81
 
82
  if (auditorResult.findings.length > 0) {
83
+ const report = mapFindingToReport(
84
+ auditorResult.findings[0],
85
+ coderResult.contract,
86
+ auditorResult.repoContext // ← now forwarded to tester
87
+ );
88
+ report.customSandboxDir = outputDir; // ← tester runs in real project sandbox
89
  const testerResult = await testerAgent.invoke({ report });
90
 
91
+
92
  await send("log", `[Tester] Execução concluída com status: ${testerResult.status}`);
93
 
94
  // Garante que o objeto enviado tem exatamente o que o front espera
src/utils/mapFinding.ts CHANGED
@@ -1,32 +1,76 @@
1
- import type { Finding, VulnerabilityReport } from "../agents/tester/types.js";
2
 
3
  /**
4
- * Mapeia um achado (Finding) do Auditor para um relatório de vulnerabilidade (VulnerabilityReport)
5
- * compatível com o Gerador de PoCs (Tester).
 
 
6
  */
7
- export function mapFindingToReport(finding: any, sourceCode: string): VulnerabilityReport {
8
- const title = finding.title || finding.type || "Unknown vulnerability";
9
- const description = finding.description || "No description provided by auditor.";
10
-
 
 
 
11
  const nameMatch = finding.path?.match(/([^\/]+)\.sol$/);
12
  const contractName = nameMatch ? nameMatch[1] : "TargetContract";
13
 
14
- const exploitablePaths = finding.judgeReview?.exploitablePaths || [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  return {
17
  id: title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 50),
18
- severity: (finding.severity === "high" || finding.severity === "medium" || finding.severity === "low")
19
- ? finding.severity : "low",
20
- type: finding.type || "custom",
21
  title,
22
- description,
23
  affectedContract: {
24
  name: contractName,
25
  sourceCode,
 
26
  },
27
- attackVector: exploitablePaths[0] ?? "Unknown vector",
28
  exploitablePaths,
29
  codeSnippet: finding.codeSnippet,
30
- location: finding.location
 
31
  };
32
  }
 
1
+ import type { VulnerabilityReport } from "../agents/tester/types.js";
2
 
3
  /**
4
+ * Maps a Finding from the Auditor to a VulnerabilityReport for the Tester.
5
+ * Enriches the description with all available auditor context:
6
+ * judge review, recommendation, exploit paths — giving the tester
7
+ * maximum information to generate a precise, specific PoC.
8
  */
9
+ export function mapFindingToReport(
10
+ finding: any,
11
+ sourceCode: string,
12
+ repoContext?: string
13
+ ): VulnerabilityReport {
14
+ const title = finding.title || "Unknown vulnerability";
15
+
16
  const nameMatch = finding.path?.match(/([^\/]+)\.sol$/);
17
  const contractName = nameMatch ? nameMatch[1] : "TargetContract";
18
 
19
+ const exploitablePaths: string[] = finding.judgeReview?.exploitablePaths || [];
20
+
21
+ // Build a rich description combining all auditor context
22
+ const descriptionParts: string[] = [
23
+ finding.description || "No description provided by auditor.",
24
+ ];
25
+
26
+ if (finding.judgeReview?.review) {
27
+ descriptionParts.push(`\n## Judge Analysis\n${finding.judgeReview.review}`);
28
+ }
29
+
30
+ if (finding.recommendation) {
31
+ descriptionParts.push(`\n## Recommended Fix\n${finding.recommendation}`);
32
+ }
33
+
34
+ if (exploitablePaths.length > 0) {
35
+ descriptionParts.push(`\n## Exploit Paths (step-by-step)\n${exploitablePaths.map((p, i) => `${i + 1}. ${p}`).join("\n")}`);
36
+ }
37
+
38
+ if (repoContext) {
39
+ descriptionParts.push(`\n## Protocol Context\n${repoContext.slice(0, 1500)}`);
40
+ }
41
+
42
+ // Infer vulnerability type from title/description when auditor doesn't provide one
43
+ const inferType = (): string => {
44
+ const text = `${title} ${finding.description || ""}`.toLowerCase();
45
+ if (text.includes("reentr")) return "reentrancy";
46
+ if (text.includes("access control") || text.includes("unauthorized")) return "access control";
47
+ if (text.includes("overflow") || text.includes("underflow")) return "arithmetic";
48
+ if (text.includes("flash loan")) return "flash loan";
49
+ if (text.includes("oracle") || text.includes("price manipul")) return "oracle manipulation";
50
+ if (text.includes("denial of service") || text.includes("dos")) return "denial of service";
51
+ if (text.includes("front.run") || text.includes("sandwich")) return "front-running";
52
+ return "logic error";
53
+ };
54
+
55
+ const severity = ["critical", "high", "medium", "low"].includes(finding.severity)
56
+ ? finding.severity
57
+ : "medium";
58
 
59
  return {
60
  id: title.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 50),
61
+ severity: severity as VulnerabilityReport["severity"],
62
+ type: inferType(),
 
63
  title,
64
+ description: descriptionParts.join("\n"),
65
  affectedContract: {
66
  name: contractName,
67
  sourceCode,
68
+ sourceFilePath: finding.path,
69
  },
70
+ attackVector: exploitablePaths[0] ?? finding.description?.slice(0, 120) ?? "Unknown",
71
  exploitablePaths,
72
  codeSnippet: finding.codeSnippet,
73
+ location: finding.location,
74
+ suggestedCheatcodes: [],
75
  };
76
  }