Spaces:
Runtime error
Runtime error
Tales-Cunha
feat: refactor tester agent to PoCo LangGraph architecture and add synthetic benchmark
017c628 | import "dotenv/config"; | |
| import fs from "fs/promises"; | |
| import path from "path"; | |
| import { execSync } from "child_process"; | |
| import { testerAgent } from "../agents/tester/agent.js"; | |
| import { VulnerabilityReport, PoCResult } from "../agents/tester/types.js"; | |
| import { setupSandbox, applyPatchSmart, computePatchDiff } from "./runTesterBenchmark.js"; | |
| const DATASET_PATH = path.join(process.cwd(), "Proof-of-Patch-only-dataset"); | |
| const TEMP_DIR = path.join(process.cwd(), "temp_eval_run"); | |
| const CSV_FILE = path.join(process.cwd(), "data", "final_evaluation_results.csv"); | |
| async function runEvaluation() { | |
| const metadataStr = await fs.readFile(path.join(DATASET_PATH, "dataset_metadata.json"), "utf8"); | |
| const metadata = JSON.parse(metadataStr); | |
| const cases = Object.keys(metadata); | |
| // Configurações Globais | |
| const MAX_CASES = 1; | |
| const TIMEOUT_MS = 3 * 60 * 1000; // 3 minutos por caso | |
| const targetCases = cases.slice(0, MAX_CASES); | |
| console.log(`Iniciando avaliação final para ${targetCases.length} projetos...`); | |
| // Prepara o arquivo CSV | |
| const csvHeaders = [ | |
| "ID", | |
| "Time_Sec", | |
| "Reproducible", | |
| "Specific", | |
| "False_Positive_Rejected", | |
| "A_Infra_Iters", | |
| "A_Exploit_Iters", | |
| "A_Final_Error", | |
| "B_Infra_Iters", | |
| "B_Exploit_Iters", | |
| "B_Final_Error", | |
| "PoC_Code", | |
| "Patch_Diff", | |
| "A_Execution_Logs", | |
| "B_Execution_Logs" | |
| ]; | |
| await fs.mkdir(path.join(process.cwd(), "data"), { recursive: true }); | |
| await fs.writeFile(CSV_FILE, csvHeaders.join(";") + "\n"); | |
| for (const caseId of targetCases) { | |
| const data = metadata[caseId]; | |
| console.log(`\n\n${"=".repeat(60)}`); | |
| console.log(`=== INICIANDO CASO: ${caseId} (${data.repo_name}) ===`); | |
| console.log(`${"=".repeat(60)}`); | |
| const startTime = Date.now(); | |
| // ========================================== | |
| // CENÁRIO A: Verdadeiro Positivo (Vulnerável) | |
| // ========================================== | |
| console.log(`\n[CENÁRIO A] Testando reprodução real e especificidade...`); | |
| const setupInfo = await setupSandbox(caseId, data); | |
| if (!setupInfo) { | |
| console.log(`[${caseId}] Falha crítica no setup inicial.`); | |
| appendCsvRow([caseId, "0", "FALSE", "FALSE", "FALSE", "0", "SETUP_FAILED", "0", "", "", "", "", ""]); | |
| continue; | |
| } | |
| const sandboxDir = setupInfo; | |
| const targetPath = data.main_contract; // Path relative to project root | |
| let vulnerableCode = ""; | |
| try { | |
| vulnerableCode = await fs.readFile(path.join(sandboxDir, targetPath), "utf8"); | |
| } catch { | |
| vulnerableCode = "// Could not load source code"; | |
| } | |
| // Ler o arquivo de teste de referência (se houver) | |
| let referenceTestCode = ""; | |
| try { | |
| const allTestFiles = execSync(`find ${path.join(sandboxDir, "test")} -name "*.t.sol" -o -name "*.sol"`, { encoding: "utf8" }) | |
| .split("\n").filter(Boolean); | |
| if (allTestFiles.length > 0) { | |
| referenceTestCode = await fs.readFile(allTestFiles[0], "utf8"); | |
| } | |
| } catch(e) {} | |
| let patchDiff = ""; | |
| try { | |
| const patchSourceDir = path.join(process.cwd(), DATASET_PATH, data.patch); | |
| const targetDir = path.join(process.cwd(), DATASET_PATH, data.target_directory); | |
| patchDiff = await computePatchDiff(patchSourceDir, path.join(sandboxDir, targetPath), targetDir, targetPath); | |
| } catch {} | |
| const reportA: VulnerabilityReport = { | |
| id: caseId, | |
| severity: data.impact || "high", | |
| type: data.expected_vulnerability, | |
| title: `${data.repo_name} - ${caseId}`, | |
| description: data.annotation, | |
| affectedContract: { name: targetPath.split("/").pop()!.replace(".sol", ""), sourceCode: vulnerableCode, sourceFilePath: targetPath }, | |
| attackVector: data.expected_vulnerability, | |
| customSandboxDir: sandboxDir, | |
| referenceTestCode, | |
| patchDiff | |
| }; | |
| const resultA = await testerAgent.invoke({ report: reportA }, { recursionLimit: 100 }) as any; | |
| let reproducible = resultA.status === "success"; | |
| let specific = false; | |
| // Ler o PoC gerado diretamente do sandbox, já que o ReAct agent usa write_file | |
| let pocCodeStr = ""; | |
| try { | |
| pocCodeStr = await fs.readFile(path.join(sandboxDir, "test", "Exploit.t.sol"), "utf8"); | |
| } catch { | |
| pocCodeStr = resultA.pocCode || ""; | |
| } | |
| // Se reproduziu, testa a especificidade aplicando o patch | |
| if (reproducible) { | |
| console.log(`\n[CENÁRIO A] Reproduzível! PoC gerado com sucesso. Testando especificidade no patch...`); | |
| const patchApplied = await applyPatchSmart(caseId, sandboxDir); | |
| if (patchApplied) { | |
| // Escreve o teste na pasta (já patcheada) | |
| await fs.writeFile(path.join(sandboxDir, "test", "Exploit.t.sol"), pocCodeStr); | |
| const { runFoundry } = await import("../agents/tester/tools/foundryRunner.js"); | |
| const specResult = await runFoundry(pocCodeStr, sandboxDir); | |
| if (specResult.exitCode !== 0) { | |
| console.log(`[CENÁRIO A] Especificidade CONFIRMADA! O teste falhou após o patch.`); | |
| specific = true; | |
| } else { | |
| console.log(`[CENÁRIO A] FALSO ESPECÍFICO! O teste continuou passando mesmo no código corrigido.`); | |
| } | |
| } else { | |
| console.log(`[CENÁRIO A] Falha ao aplicar patch. Assumindo especificidade FALSA.`); | |
| } | |
| } | |
| const lastErrorA = (resultA as any).lastError || (resultA.status === "success" ? "" : "TIMEOUT"); | |
| const logsA = Buffer.from(((resultA as any).executionLogs || []).join("\n---\n")).toString("base64"); | |
| // ========================================== | |
| // CENÁRIO B: Teste de Falso Positivo (Patch) | |
| // ========================================== | |
| console.log(`\n[CENÁRIO B] Testando rejeição de falso positivo (Robustez)...`); | |
| // Recria a sandbox do zero | |
| await execSync(`rm -rf ${sandboxDir}`); | |
| const setupInfoB = await setupSandbox(caseId, data); | |
| let falsePositiveRejected = false; | |
| let resultB: Partial<PoCResult> = { iterations: 0, status: "failed" }; | |
| let lastErrorB = ""; | |
| let logsB = ""; | |
| if (setupInfoB) { | |
| // Aplica o patch ANTES de chamar o agente (tornando o código seguro) | |
| const patchAppliedB = await applyPatchSmart(caseId, sandboxDir); | |
| if (patchAppliedB) { | |
| let patchedCode = ""; | |
| try { | |
| patchedCode = await fs.readFile(path.join(sandboxDir, data.main_contract), "utf8"); | |
| } catch { | |
| patchedCode = "// Could not load patched code"; | |
| } | |
| // Passa a MESMA anotação (mentindo que é vulnerável) | |
| const reportB: VulnerabilityReport = { | |
| ...reportA, | |
| affectedContract: { name: targetPath.split("/").pop()!.replace(".sol", ""), sourceCode: patchedCode, sourceFilePath: targetPath }, | |
| patchDiff: undefined // Oculta o patch diff do LLM para este cenário | |
| }; | |
| resultB = await testerAgent.invoke({ report: reportB }, { recursionLimit: 100 }) as any; | |
| // Se falhou em gerar exploit, REJEITOU com sucesso o falso positivo! | |
| if (resultB.status !== "success") { | |
| console.log(`\n[CENÁRIO B] SUCESSO DE ROBUSTEZ! Agente não conseguiu hackear o código seguro.`); | |
| falsePositiveRejected = true; | |
| } else { | |
| console.log(`\n[CENÁRIO B] ALUCINAÇÃO CRÍTICA! Agente hackeou um código que já estava corrigido.`); | |
| } | |
| lastErrorB = (resultB as any).lastError || (resultB.status === "success" ? "" : "TIMEOUT"); | |
| logsB = Buffer.from(((resultB as any).executionLogs || []).join("\n---\n")).toString("base64"); | |
| } | |
| } | |
| const totalTimeSec = Math.floor((Date.now() - startTime) / 1000); | |
| // Salva no CSV | |
| appendCsvRow([ | |
| caseId, | |
| totalTimeSec.toString(), | |
| reproducible ? "TRUE" : "FALSE", | |
| specific ? "TRUE" : "FALSE", | |
| falsePositiveRejected ? "TRUE" : "FALSE", | |
| (resultA as any).infraIterations || 0, | |
| (resultA as any).exploitIterations || 0, | |
| lastErrorA, | |
| (resultB as any).infraIterations || 0, | |
| (resultB as any).exploitIterations || 0, | |
| lastErrorB, | |
| Buffer.from(pocCodeStr).toString("base64"), | |
| reproducible ? Buffer.from(patchDiff).toString("base64") : "", | |
| logsA, | |
| logsB | |
| ]); | |
| console.log(`[${caseId}] Avaliação concluída em ${totalTimeSec}s. Salvo no CSV.`); | |
| } | |
| console.log(`\nAVALIAÇÃO FINAL CONCLUÍDA! Resultados em: ${CSV_FILE}`); | |
| } | |
| function escapeCsv(str: string) { | |
| if (!str) return ""; | |
| // Troca aspas duplas por duplas aspas duplas (padrão CSV) | |
| return `"${str.replace(/"/g, '""')}"`; | |
| } | |
| async function appendCsvRow(columns: string[]) { | |
| const row = columns.join(";") + "\n"; | |
| await fs.appendFile(CSV_FILE, row); | |
| } | |
| runEvaluation().catch(console.error); | |