Spaces:
Runtime error
Runtime error
Uanderson Silva commited on
Commit ·
48c7a9b
1
Parent(s): 4c144c5
unify logs under custom logger
Browse files- src/agents/auditor/agent.ts +17 -17
- src/agents/tester/agent.ts +32 -33
- src/agents/tester/index.ts +7 -6
- src/agents/tester/tools/foundryRunner.ts +13 -12
- src/config/llm.ts +0 -2
- src/logger.ts +27 -0
- src/server.ts +16 -13
src/agents/auditor/agent.ts
CHANGED
|
@@ -57,7 +57,7 @@ const walkDirectory = (dir: string, depth: number, solFiles: string[], docFiles:
|
|
| 57 |
};
|
| 58 |
|
| 59 |
const defineScope: GraphNode<typeof AuditorState> = async (state) => {
|
| 60 |
-
logger.info(`defineScope:
|
| 61 |
|
| 62 |
const solFiles: string[] = [];
|
| 63 |
const docFiles: string[] = [];
|
|
@@ -66,16 +66,16 @@ const defineScope: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 66 |
|
| 67 |
const fileTree = buildRepoTree(state.repoPath);
|
| 68 |
|
| 69 |
-
logger.info(`defineScope:
|
| 70 |
-
logger.debug(`defineScope:
|
| 71 |
-
logger.debug(`defineScope:
|
| 72 |
-
logger.debug(`defineScope:
|
| 73 |
|
| 74 |
return { scope: solFiles, docs: docFiles, fileTree };
|
| 75 |
};
|
| 76 |
|
| 77 |
const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
|
| 78 |
-
logger.info(`gatherContext:
|
| 79 |
|
| 80 |
const readFile = (filePath: string): string => {
|
| 81 |
try {
|
|
@@ -124,8 +124,8 @@ const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 124 |
const model = llm.withStructuredOutput(z.object({ context: z.string() }));
|
| 125 |
const result = await model.invoke([new SystemMessage(GATHER_CONTEXT_PROMPT), new HumanMessage(parts.join("\n\n"))]);
|
| 126 |
|
| 127 |
-
logger.info(`gatherContext:
|
| 128 |
-
logger.debug(`gatherContext:
|
| 129 |
|
| 130 |
return { repoContext: result.context };
|
| 131 |
};
|
|
@@ -144,7 +144,7 @@ const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 144 |
: null;
|
| 145 |
|
| 146 |
logger.info(
|
| 147 |
-
`findVulnerabilities:
|
| 148 |
);
|
| 149 |
|
| 150 |
const allFindings = await Promise.all(
|
|
@@ -162,7 +162,7 @@ const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 162 |
userMessage += `\n\nJudge feedback from previous iteration (iteration ${state.reflectionCount}):\n${previousFeedback}\n\nRevise your findings accordingly.`;
|
| 163 |
}
|
| 164 |
|
| 165 |
-
logger.debug(`findVulnerabilities:
|
| 166 |
|
| 167 |
const result = await model.invoke([
|
| 168 |
new SystemMessage(FIND_VULNERABILITIES_PROMPT),
|
|
@@ -178,15 +178,15 @@ const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 178 |
);
|
| 179 |
|
| 180 |
const candidateFindings = allFindings.flat();
|
| 181 |
-
logger.info(`findVulnerabilities: LLM
|
| 182 |
-
logger.debug(`findVulnerabilities: findings:\n${JSON.stringify(candidateFindings, null, 2)}`);
|
| 183 |
|
| 184 |
return { candidateFindings };
|
| 185 |
};
|
| 186 |
|
| 187 |
const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
|
| 188 |
if (state.candidateFindings.length === 0) {
|
| 189 |
-
logger.info("judgeFindings:
|
| 190 |
return {
|
| 191 |
judgeReviews: [],
|
| 192 |
findings: [],
|
|
@@ -196,7 +196,7 @@ const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 196 |
|
| 197 |
const model = llm.withStructuredOutput(JudgeReviewSchema);
|
| 198 |
|
| 199 |
-
logger.info(`judgeFindings:
|
| 200 |
|
| 201 |
const reviews = await Promise.all(
|
| 202 |
state.candidateFindings.map(async (finding, i) => {
|
|
@@ -209,7 +209,7 @@ const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 209 |
|
| 210 |
const findingText = `[Finding ${i + 1}] ${finding.title}\nSeverity: ${finding.severity}\nDescription: ${finding.description}\nLocation: ${finding.path} lines ${finding.location}\nCode:\n\`\`\`solidity\n${finding.codeSnippet}\n\`\`\``;
|
| 211 |
|
| 212 |
-
logger.debug(`judgeFindings:
|
| 213 |
return model.invoke([
|
| 214 |
new SystemMessage(JUDGE_FINDINGS_PROMPT),
|
| 215 |
new HumanMessage(
|
|
@@ -234,8 +234,8 @@ const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 234 |
|
| 235 |
const falsePositiveCount = state.candidateFindings.length - findings.length;
|
| 236 |
|
| 237 |
-
logger.info(`judgeFindings: ${findings.length}
|
| 238 |
-
logger.debug(`judgeFindings:
|
| 239 |
|
| 240 |
return {
|
| 241 |
judgeReviews: reviews,
|
|
|
|
| 57 |
};
|
| 58 |
|
| 59 |
const defineScope: GraphNode<typeof AuditorState> = async (state) => {
|
| 60 |
+
logger.info(`[Auditor] defineScope: percorrendo repositório em ${state.repoPath}`);
|
| 61 |
|
| 62 |
const solFiles: string[] = [];
|
| 63 |
const docFiles: string[] = [];
|
|
|
|
| 66 |
|
| 67 |
const fileTree = buildRepoTree(state.repoPath);
|
| 68 |
|
| 69 |
+
logger.info(`[Auditor] defineScope: encontrado(s) ${solFiles.length} arquivo(s) Solidity e ${docFiles.length} arquivo(s) de documentação`);
|
| 70 |
+
logger.debug(`[Auditor] defineScope: arquivos Solidity: ${JSON.stringify(solFiles)}`);
|
| 71 |
+
logger.debug(`[Auditor] defineScope: arquivos de documentação: ${JSON.stringify(docFiles)}`);
|
| 72 |
+
logger.debug(`[Auditor] defineScope: árvore de arquivos:\n${fileTree}`);
|
| 73 |
|
| 74 |
return { scope: solFiles, docs: docFiles, fileTree };
|
| 75 |
};
|
| 76 |
|
| 77 |
const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
|
| 78 |
+
logger.info(`[Auditor] gatherContext: processando ${state.scope.length} arquivo(s) Solidity e ${state.docs.length} arquivo(s) de documentação`);
|
| 79 |
|
| 80 |
const readFile = (filePath: string): string => {
|
| 81 |
try {
|
|
|
|
| 124 |
const model = llm.withStructuredOutput(z.object({ context: z.string() }));
|
| 125 |
const result = await model.invoke([new SystemMessage(GATHER_CONTEXT_PROMPT), new HumanMessage(parts.join("\n\n"))]);
|
| 126 |
|
| 127 |
+
logger.info(`[Auditor] gatherContext: contexto construído (${parts.join("\n\n").length} caracteres)`);
|
| 128 |
+
logger.debug(`[Auditor] gatherContext: contexto completo:\n${parts.join("\n\n")}`);
|
| 129 |
|
| 130 |
return { repoContext: result.context };
|
| 131 |
};
|
|
|
|
| 144 |
: null;
|
| 145 |
|
| 146 |
logger.info(
|
| 147 |
+
`[Auditor] findVulnerabilities: invocando LLM para ${state.scope.length} arquivo(s) em paralelo (iteração ${state.reflectionCount + 1})`,
|
| 148 |
);
|
| 149 |
|
| 150 |
const allFindings = await Promise.all(
|
|
|
|
| 162 |
userMessage += `\n\nJudge feedback from previous iteration (iteration ${state.reflectionCount}):\n${previousFeedback}\n\nRevise your findings accordingly.`;
|
| 163 |
}
|
| 164 |
|
| 165 |
+
logger.debug(`[Auditor] findVulnerabilities: processando ${filePath}`);
|
| 166 |
|
| 167 |
const result = await model.invoke([
|
| 168 |
new SystemMessage(FIND_VULNERABILITIES_PROMPT),
|
|
|
|
| 178 |
);
|
| 179 |
|
| 180 |
const candidateFindings = allFindings.flat();
|
| 181 |
+
logger.info(`[Auditor] findVulnerabilities: LLM retornou ${candidateFindings.length} finding(s) candidato(s) no total`);
|
| 182 |
+
logger.debug(`[Auditor] findVulnerabilities: findings:\n${JSON.stringify(candidateFindings, null, 2)}`);
|
| 183 |
|
| 184 |
return { candidateFindings };
|
| 185 |
};
|
| 186 |
|
| 187 |
const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
|
| 188 |
if (state.candidateFindings.length === 0) {
|
| 189 |
+
logger.info("[Auditor] judgeFindings: sem findings candidatos para revisar, pulando chamada ao LLM");
|
| 190 |
return {
|
| 191 |
judgeReviews: [],
|
| 192 |
findings: [],
|
|
|
|
| 196 |
|
| 197 |
const model = llm.withStructuredOutput(JudgeReviewSchema);
|
| 198 |
|
| 199 |
+
logger.info(`[Auditor] judgeFindings: revisando ${state.candidateFindings.length} finding(s) candidato(s) em paralelo`);
|
| 200 |
|
| 201 |
const reviews = await Promise.all(
|
| 202 |
state.candidateFindings.map(async (finding, i) => {
|
|
|
|
| 209 |
|
| 210 |
const findingText = `[Finding ${i + 1}] ${finding.title}\nSeverity: ${finding.severity}\nDescription: ${finding.description}\nLocation: ${finding.path} lines ${finding.location}\nCode:\n\`\`\`solidity\n${finding.codeSnippet}\n\`\`\``;
|
| 211 |
|
| 212 |
+
logger.debug(`[Auditor] judgeFindings: revisando finding ${i + 1}: ${finding.title}`);
|
| 213 |
return model.invoke([
|
| 214 |
new SystemMessage(JUDGE_FINDINGS_PROMPT),
|
| 215 |
new HumanMessage(
|
|
|
|
| 234 |
|
| 235 |
const falsePositiveCount = state.candidateFindings.length - findings.length;
|
| 236 |
|
| 237 |
+
logger.info(`[Auditor] judgeFindings: ${findings.length} confirmado(s), ${falsePositiveCount} falso(s) positivo(s)`);
|
| 238 |
+
logger.debug(`[Auditor] judgeFindings: revisões:\n${JSON.stringify(reviews, null, 2)}`);
|
| 239 |
|
| 240 |
return {
|
| 241 |
judgeReviews: reviews,
|
src/agents/tester/agent.ts
CHANGED
|
@@ -1,25 +1,26 @@
|
|
| 1 |
-
import "dotenv/config";
|
| 2 |
import { StateGraph, END, START } from "@langchain/langgraph";
|
| 3 |
-
|
|
|
|
| 4 |
import { generateLocalScaffold } from "./tools/scaffoldGenerator.js";
|
| 5 |
-
import { OracleContext } from "./types.js";
|
| 6 |
import { createLLM } from "../../config/llm.ts";
|
| 7 |
import { SYSTEM_PROMPT } from "./prompts/system.js";
|
| 8 |
import { extractSolidity } from "./utils/extractSolidity.js";
|
| 9 |
import { runFoundry } from "./tools/foundryRunner.js";
|
| 10 |
import { analyzeFoundryLog } from "./utils/logAnalyzer.js";
|
|
|
|
| 11 |
|
| 12 |
const MAX_ITERATIONS = 5;
|
| 13 |
|
| 14 |
const llm = createLLM();
|
| 15 |
|
| 16 |
async function oracleNode(state: PoCState): Promise<Partial<PoCState>> {
|
| 17 |
-
|
| 18 |
|
| 19 |
const solidityScaffold = generateLocalScaffold(state.report);
|
| 20 |
const oracleContext: OracleContext = { solidityScaffold };
|
| 21 |
|
| 22 |
-
|
| 23 |
return { oracleContext };
|
| 24 |
}
|
| 25 |
|
|
@@ -53,7 +54,7 @@ Scaffold (complete APENAS test_Exploit):
|
|
| 53 |
${oracleContext!.solidityScaffold}
|
| 54 |
\`\`\``;
|
| 55 |
|
| 56 |
-
|
| 57 |
|
| 58 |
try {
|
| 59 |
const response = await llm.invoke([
|
|
@@ -61,16 +62,16 @@ ${oracleContext!.solidityScaffold}
|
|
| 61 |
{ role: "user", content: userMessage },
|
| 62 |
]);
|
| 63 |
const solidityCode = extractSolidity(response.content as string);
|
| 64 |
-
|
| 65 |
return { pocCode: solidityCode, iterations: 1 };
|
| 66 |
} catch (err) {
|
| 67 |
-
|
| 68 |
return { iterations: 1, lastError: `Erro na geração/extração: ${(err as Error).message}` };
|
| 69 |
}
|
| 70 |
}
|
| 71 |
|
| 72 |
async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
|
| 73 |
-
|
| 74 |
|
| 75 |
const trimmedCode = state.pocCode.trim();
|
| 76 |
const isMissingCode = trimmedCode.length === 0;
|
|
@@ -78,14 +79,15 @@ async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
|
|
| 78 |
const isMissingTest = !trimmedCode.includes("function test_Exploit()");
|
| 79 |
const isPlaceholder = trimmedCode.includes("TODO: implementar exploit");
|
| 80 |
if (isMissingCode || isMissingContract || isMissingTest || isPlaceholder) {
|
| 81 |
-
const summary =
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
? "
|
| 85 |
-
:
|
| 86 |
-
? "
|
| 87 |
-
:
|
| 88 |
-
|
|
|
|
| 89 |
const status = state.iterations >= MAX_ITERATIONS ? "failed" : "running";
|
| 90 |
return {
|
| 91 |
executionLogs: [summary],
|
|
@@ -94,30 +96,24 @@ async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
|
|
| 94 |
};
|
| 95 |
}
|
| 96 |
|
| 97 |
-
const result
|
| 98 |
const analysis = analyzeFoundryLog(result);
|
| 99 |
const noTestsFound = result.combined.includes("No tests found");
|
| 100 |
const summary = noTestsFound
|
| 101 |
? "Forge não encontrou nenhum teste. Verifique se o contrato se chama ExploitTest e se existe test_Exploit()."
|
| 102 |
: analysis.summary;
|
| 103 |
-
const passed
|
| 104 |
const isLastAttempt = state.iterations >= MAX_ITERATIONS;
|
| 105 |
|
| 106 |
-
const status = passed
|
| 107 |
-
? "success"
|
| 108 |
-
: result.timedOut
|
| 109 |
-
? "timeout"
|
| 110 |
-
: isLastAttempt
|
| 111 |
-
? "failed"
|
| 112 |
-
: "running";
|
| 113 |
|
| 114 |
-
|
| 115 |
if (!passed) {
|
| 116 |
-
|
| 117 |
}
|
| 118 |
|
| 119 |
return {
|
| 120 |
-
executionLogs: [result.combined],
|
| 121 |
lastError: summary,
|
| 122 |
status,
|
| 123 |
};
|
|
@@ -130,14 +126,17 @@ async function reflectNode(state: PoCState): Promise<Partial<PoCState>> {
|
|
| 130 |
}
|
| 131 |
|
| 132 |
const mockResult = {
|
| 133 |
-
exitCode: 1,
|
| 134 |
-
|
|
|
|
|
|
|
|
|
|
| 135 |
};
|
| 136 |
|
| 137 |
const analysis = analyzeFoundryLog(mockResult as any);
|
| 138 |
|
| 139 |
-
|
| 140 |
-
|
| 141 |
|
| 142 |
return {
|
| 143 |
lastError: `[${analysis.category.toUpperCase()}] ${analysis.summary}\n\nLinhas relevantes:\n${analysis.relevantLines.join("\n")}`,
|
|
|
|
|
|
|
| 1 |
import { StateGraph, END, START } from "@langchain/langgraph";
|
| 2 |
+
|
| 3 |
+
import { PoCStateAnnotation, type PoCState } from "./state.js";
|
| 4 |
import { generateLocalScaffold } from "./tools/scaffoldGenerator.js";
|
| 5 |
+
import type { OracleContext } from "./types.js";
|
| 6 |
import { createLLM } from "../../config/llm.ts";
|
| 7 |
import { SYSTEM_PROMPT } from "./prompts/system.js";
|
| 8 |
import { extractSolidity } from "./utils/extractSolidity.js";
|
| 9 |
import { runFoundry } from "./tools/foundryRunner.js";
|
| 10 |
import { analyzeFoundryLog } from "./utils/logAnalyzer.js";
|
| 11 |
+
import { logger } from "../../logger.ts";
|
| 12 |
|
| 13 |
const MAX_ITERATIONS = 5;
|
| 14 |
|
| 15 |
const llm = createLLM();
|
| 16 |
|
| 17 |
async function oracleNode(state: PoCState): Promise<Partial<PoCState>> {
|
| 18 |
+
logger.info(`[Tester] oracleNode: gerando scaffold para: ${state.report.title}`);
|
| 19 |
|
| 20 |
const solidityScaffold = generateLocalScaffold(state.report);
|
| 21 |
const oracleContext: OracleContext = { solidityScaffold };
|
| 22 |
|
| 23 |
+
logger.info(`[Tester] oracleNode: scaffold gerado, tamanho: ${solidityScaffold.length} chars`);
|
| 24 |
return { oracleContext };
|
| 25 |
}
|
| 26 |
|
|
|
|
| 54 |
${oracleContext!.solidityScaffold}
|
| 55 |
\`\`\``;
|
| 56 |
|
| 57 |
+
logger.info(`[Tester] generatePoCNode: iteração ${iterations + 1}, isRetry=${isRetry}`);
|
| 58 |
|
| 59 |
try {
|
| 60 |
const response = await llm.invoke([
|
|
|
|
| 62 |
{ role: "user", content: userMessage },
|
| 63 |
]);
|
| 64 |
const solidityCode = extractSolidity(response.content as string);
|
| 65 |
+
logger.info(`[Tester] generatePoCNode: Solidity extraído, tamanho: ${solidityCode.length}`);
|
| 66 |
return { pocCode: solidityCode, iterations: 1 };
|
| 67 |
} catch (err) {
|
| 68 |
+
logger.error(`[Tester] generatePoCNode: falha na geração: ${(err as Error).message}`);
|
| 69 |
return { iterations: 1, lastError: `Erro na geração/extração: ${(err as Error).message}` };
|
| 70 |
}
|
| 71 |
}
|
| 72 |
|
| 73 |
async function runFoundryNode(state: PoCState): Promise<Partial<PoCState>> {
|
| 74 |
+
logger.info("[Tester] runFoundryNode: executando...");
|
| 75 |
|
| 76 |
const trimmedCode = state.pocCode.trim();
|
| 77 |
const isMissingCode = trimmedCode.length === 0;
|
|
|
|
| 79 |
const isMissingTest = !trimmedCode.includes("function test_Exploit()");
|
| 80 |
const isPlaceholder = trimmedCode.includes("TODO: implementar exploit");
|
| 81 |
if (isMissingCode || isMissingContract || isMissingTest || isPlaceholder) {
|
| 82 |
+
const summary =
|
| 83 |
+
state.lastError ??
|
| 84 |
+
(isMissingCode
|
| 85 |
+
? "Código Solidity ausente. O LLM não retornou o arquivo do exploit."
|
| 86 |
+
: isMissingContract
|
| 87 |
+
? "Contrato ExploitTest não encontrado no arquivo."
|
| 88 |
+
: isMissingTest
|
| 89 |
+
? "Função test_Exploit() não encontrada no arquivo."
|
| 90 |
+
: "Exploit não implementado (placeholder TODO ainda presente).");
|
| 91 |
const status = state.iterations >= MAX_ITERATIONS ? "failed" : "running";
|
| 92 |
return {
|
| 93 |
executionLogs: [summary],
|
|
|
|
| 96 |
};
|
| 97 |
}
|
| 98 |
|
| 99 |
+
const result = await runFoundry(state.pocCode);
|
| 100 |
const analysis = analyzeFoundryLog(result);
|
| 101 |
const noTestsFound = result.combined.includes("No tests found");
|
| 102 |
const summary = noTestsFound
|
| 103 |
? "Forge não encontrou nenhum teste. Verifique se o contrato se chama ExploitTest e se existe test_Exploit()."
|
| 104 |
: analysis.summary;
|
| 105 |
+
const passed = result.exitCode === 0 && result.stdout.includes("ok") && !noTestsFound;
|
| 106 |
const isLastAttempt = state.iterations >= MAX_ITERATIONS;
|
| 107 |
|
| 108 |
+
const status = passed ? "success" : result.timedOut ? "timeout" : isLastAttempt ? "failed" : "running";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
|
| 110 |
+
logger.info(`[Tester] runFoundryNode: resultado Foundry: exitCode=${result.exitCode}, passed=${passed}`);
|
| 111 |
if (!passed) {
|
| 112 |
+
logger.info(`[Tester] runFoundryNode: falha detectada: ${analysis.summary}`);
|
| 113 |
}
|
| 114 |
|
| 115 |
return {
|
| 116 |
+
executionLogs: [result.combined], // reducer append
|
| 117 |
lastError: summary,
|
| 118 |
status,
|
| 119 |
};
|
|
|
|
| 126 |
}
|
| 127 |
|
| 128 |
const mockResult = {
|
| 129 |
+
exitCode: 1,
|
| 130 |
+
timedOut: lastLog.includes("TIMEOUT"),
|
| 131 |
+
stdout: "",
|
| 132 |
+
stderr: "",
|
| 133 |
+
combined: lastLog,
|
| 134 |
};
|
| 135 |
|
| 136 |
const analysis = analyzeFoundryLog(mockResult as any);
|
| 137 |
|
| 138 |
+
logger.info(`[Tester] reflectNode: categoria: ${analysis.category}`);
|
| 139 |
+
logger.info(`[Tester] reflectNode: resumo: ${analysis.summary}`);
|
| 140 |
|
| 141 |
return {
|
| 142 |
lastError: `[${analysis.category.toUpperCase()}] ${analysis.summary}\n\nLinhas relevantes:\n${analysis.relevantLines.join("\n")}`,
|
src/agents/tester/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import { testerAgent } from "./agent.js";
|
| 2 |
import { VulnerabilityReport, PoCResult } from "./types.js";
|
|
|
|
| 3 |
|
| 4 |
/**
|
| 5 |
* Entry point para o Agente Gerador de PoCs.
|
|
@@ -7,19 +8,19 @@ import { VulnerabilityReport, PoCResult } from "./types.js";
|
|
| 7 |
* @returns PoCResult contendo o código do exploit e o status da execução.
|
| 8 |
*/
|
| 9 |
export async function runPoCGenerator(report: VulnerabilityReport): Promise<PoCResult> {
|
| 10 |
-
|
| 11 |
|
| 12 |
const finalState = await testerAgent.invoke({ report });
|
| 13 |
|
| 14 |
const result: PoCResult = {
|
| 15 |
-
reportId:
|
| 16 |
-
status:
|
| 17 |
-
solidityCode:
|
| 18 |
executionLogs: finalState.executionLogs,
|
| 19 |
-
iterations:
|
| 20 |
};
|
| 21 |
|
| 22 |
-
|
| 23 |
return result;
|
| 24 |
}
|
| 25 |
|
|
|
|
| 1 |
import { testerAgent } from "./agent.js";
|
| 2 |
import { VulnerabilityReport, PoCResult } from "./types.js";
|
| 3 |
+
import { logger } from "../../logger.js";
|
| 4 |
|
| 5 |
/**
|
| 6 |
* Entry point para o Agente Gerador de PoCs.
|
|
|
|
| 8 |
* @returns PoCResult contendo o código do exploit e o status da execução.
|
| 9 |
*/
|
| 10 |
export async function runPoCGenerator(report: VulnerabilityReport): Promise<PoCResult> {
|
| 11 |
+
logger.info(`[Tester] runPoCGenerator: iniciando para: ${report.id} — ${report.title}`);
|
| 12 |
|
| 13 |
const finalState = await testerAgent.invoke({ report });
|
| 14 |
|
| 15 |
const result: PoCResult = {
|
| 16 |
+
reportId: report.id,
|
| 17 |
+
status: finalState.status === "running" ? "failed" : finalState.status,
|
| 18 |
+
solidityCode: finalState.pocCode,
|
| 19 |
executionLogs: finalState.executionLogs,
|
| 20 |
+
iterations: finalState.iterations,
|
| 21 |
};
|
| 22 |
|
| 23 |
+
logger.info(`[Tester] runPoCGenerator: concluído — status=${result.status}, iterações=${result.iterations}`);
|
| 24 |
return result;
|
| 25 |
}
|
| 26 |
|
src/agents/tester/tools/foundryRunner.ts
CHANGED
|
@@ -3,7 +3,9 @@ import { promisify } from "util";
|
|
| 3 |
import { writeFile, access } from "fs/promises";
|
| 4 |
import { join } from "path";
|
| 5 |
|
| 6 |
-
|
|
|
|
|
|
|
| 7 |
const SANDBOX = process.env.SANDBOX_DIR || "/tmp/poc-sandbox";
|
| 8 |
const TIMEOUT_MS = 60_000;
|
| 9 |
|
|
@@ -22,7 +24,7 @@ async function ensureSandbox() {
|
|
| 22 |
try {
|
| 23 |
await access(join(SANDBOX, "foundry.toml"));
|
| 24 |
} catch {
|
| 25 |
-
|
| 26 |
// Caminho absoluto para o script de setup (assume execução da raiz do projeto)
|
| 27 |
await execAsync("./scripts/setup-sandbox.sh");
|
| 28 |
}
|
|
@@ -30,19 +32,16 @@ async function ensureSandbox() {
|
|
| 30 |
|
| 31 |
export async function runFoundry(solidityCode: string): Promise<FoundryResult> {
|
| 32 |
await ensureSandbox();
|
| 33 |
-
|
| 34 |
// Escrever o arquivo no sandbox
|
| 35 |
await writeFile(`${SANDBOX}/test/Exploit.t.sol`, solidityCode, "utf-8");
|
| 36 |
|
| 37 |
try {
|
| 38 |
-
const { stdout, stderr } = await execAsync(
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
env: { ...process.env, PATH: `${process.env.HOME}/.foundry/bin:${process.env.PATH}` }
|
| 44 |
-
}
|
| 45 |
-
);
|
| 46 |
return {
|
| 47 |
exitCode: 0,
|
| 48 |
stdout,
|
|
@@ -53,7 +52,9 @@ export async function runFoundry(solidityCode: string): Promise<FoundryResult> {
|
|
| 53 |
} catch (err: any) {
|
| 54 |
if (err.killed || err.signal === "SIGTERM") {
|
| 55 |
return {
|
| 56 |
-
exitCode: -1,
|
|
|
|
|
|
|
| 57 |
combined: `TIMEOUT após ${TIMEOUT_MS / 1000}s`,
|
| 58 |
timedOut: true,
|
| 59 |
};
|
|
|
|
| 3 |
import { writeFile, access } from "fs/promises";
|
| 4 |
import { join } from "path";
|
| 5 |
|
| 6 |
+
import { logger } from "../../../logger.js";
|
| 7 |
+
|
| 8 |
+
const execAsync = promisify(exec);
|
| 9 |
const SANDBOX = process.env.SANDBOX_DIR || "/tmp/poc-sandbox";
|
| 10 |
const TIMEOUT_MS = 60_000;
|
| 11 |
|
|
|
|
| 24 |
try {
|
| 25 |
await access(join(SANDBOX, "foundry.toml"));
|
| 26 |
} catch {
|
| 27 |
+
logger.info("[Tester] foundryRunner: sandbox não encontrado, inicializando...");
|
| 28 |
// Caminho absoluto para o script de setup (assume execução da raiz do projeto)
|
| 29 |
await execAsync("./scripts/setup-sandbox.sh");
|
| 30 |
}
|
|
|
|
| 32 |
|
| 33 |
export async function runFoundry(solidityCode: string): Promise<FoundryResult> {
|
| 34 |
await ensureSandbox();
|
| 35 |
+
|
| 36 |
// Escrever o arquivo no sandbox
|
| 37 |
await writeFile(`${SANDBOX}/test/Exploit.t.sol`, solidityCode, "utf-8");
|
| 38 |
|
| 39 |
try {
|
| 40 |
+
const { stdout, stderr } = await execAsync("forge test --match-contract ExploitTest -vvvv", {
|
| 41 |
+
cwd: SANDBOX,
|
| 42 |
+
timeout: TIMEOUT_MS,
|
| 43 |
+
env: { ...process.env, PATH: `${process.env.HOME}/.foundry/bin:${process.env.PATH}` },
|
| 44 |
+
});
|
|
|
|
|
|
|
|
|
|
| 45 |
return {
|
| 46 |
exitCode: 0,
|
| 47 |
stdout,
|
|
|
|
| 52 |
} catch (err: any) {
|
| 53 |
if (err.killed || err.signal === "SIGTERM") {
|
| 54 |
return {
|
| 55 |
+
exitCode: -1,
|
| 56 |
+
stdout: "",
|
| 57 |
+
stderr: "Forge timed out",
|
| 58 |
combined: `TIMEOUT após ${TIMEOUT_MS / 1000}s`,
|
| 59 |
timedOut: true,
|
| 60 |
};
|
src/config/llm.ts
CHANGED
|
@@ -16,8 +16,6 @@ export function createLLM(overrideProvider?: LLMProvider): BaseChatModel {
|
|
| 16 |
apiKey: process.env.OPENROUTER_API_KEY,
|
| 17 |
maxTokens: 4096,
|
| 18 |
});
|
| 19 |
-
|
| 20 |
-
|
| 21 |
case "anthropic":
|
| 22 |
return new ChatAnthropic({
|
| 23 |
model: process.env.ANTHROPIC_MODEL || "claude-sonnet-4-6",
|
|
|
|
| 16 |
apiKey: process.env.OPENROUTER_API_KEY,
|
| 17 |
maxTokens: 4096,
|
| 18 |
});
|
|
|
|
|
|
|
| 19 |
case "anthropic":
|
| 20 |
return new ChatAnthropic({
|
| 21 |
model: process.env.ANTHROPIC_MODEL || "claude-sonnet-4-6",
|
src/logger.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import fs from "node:fs";
|
| 2 |
import path from "node:path";
|
|
|
|
| 3 |
|
| 4 |
import winston from "winston";
|
| 5 |
|
|
@@ -15,6 +16,27 @@ const lineFormat = printf(({ level, message, timestamp: ts, stack }) => {
|
|
| 15 |
return stack ? `${base}\n${stack}` : base;
|
| 16 |
});
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
export const logger = winston.createLogger({
|
| 19 |
level: "debug",
|
| 20 |
transports: [
|
|
@@ -25,5 +47,10 @@ export const logger = winston.createLogger({
|
|
| 25 |
filename: path.join(logsDir, `app-${runTimestamp}.log`),
|
| 26 |
format: combine(timestamp(), errors({ stack: true }), lineFormat),
|
| 27 |
}),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
],
|
| 29 |
});
|
|
|
|
| 1 |
import fs from "node:fs";
|
| 2 |
import path from "node:path";
|
| 3 |
+
import { Writable } from "node:stream";
|
| 4 |
|
| 5 |
import winston from "winston";
|
| 6 |
|
|
|
|
| 16 |
return stack ? `${base}\n${stack}` : base;
|
| 17 |
});
|
| 18 |
|
| 19 |
+
type LogSink = (message: string) => void | Promise<void>;
|
| 20 |
+
|
| 21 |
+
let activeSink: LogSink | null = null;
|
| 22 |
+
|
| 23 |
+
export function setLogSink(sink: LogSink): void {
|
| 24 |
+
activeSink = sink;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
export function clearLogSink(): void {
|
| 28 |
+
activeSink = null;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const sinkStream = new Writable({
|
| 32 |
+
write(chunk: Buffer, _encoding: string, callback: () => void) {
|
| 33 |
+
if (activeSink) {
|
| 34 |
+
void activeSink(chunk.toString().trim());
|
| 35 |
+
}
|
| 36 |
+
callback();
|
| 37 |
+
},
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
export const logger = winston.createLogger({
|
| 41 |
level: "debug",
|
| 42 |
transports: [
|
|
|
|
| 47 |
filename: path.join(logsDir, `app-${runTimestamp}.log`),
|
| 48 |
format: combine(timestamp(), errors({ stack: true }), lineFormat),
|
| 49 |
}),
|
| 50 |
+
new winston.transports.Stream({
|
| 51 |
+
stream: sinkStream,
|
| 52 |
+
level: "info",
|
| 53 |
+
format: winston.format.printf(({ message }) => String(message)),
|
| 54 |
+
}),
|
| 55 |
],
|
| 56 |
});
|
src/server.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { coderAgent } from "./agents/coder/agent.ts";
|
|
| 13 |
import { auditorAgent } from "./agents/auditor/agent.ts";
|
| 14 |
import { testerAgent } from "./agents/tester/agent.ts";
|
| 15 |
import { mapFindingToReport } from "./utils/mapFinding.js";
|
|
|
|
| 16 |
|
| 17 |
const app = new Hono();
|
| 18 |
|
|
@@ -35,16 +36,16 @@ app.post("/api/run", (c) => {
|
|
| 35 |
};
|
| 36 |
|
| 37 |
try {
|
|
|
|
|
|
|
| 38 |
// === CODER ===
|
| 39 |
-
|
| 40 |
const coderResult = await coderAgent.invoke({ requirements: [requirements] });
|
| 41 |
|
| 42 |
-
await send("log", "[Coder] Contrato gerado com sucesso.");
|
| 43 |
-
|
| 44 |
if (coderResult.compilationErrors.length > 0) {
|
| 45 |
-
|
| 46 |
} else {
|
| 47 |
-
|
| 48 |
}
|
| 49 |
|
| 50 |
await send(
|
|
@@ -62,11 +63,11 @@ app.post("/api/run", (c) => {
|
|
| 62 |
writeFileSync(resolve(outputDir, "Contract.sol"), coderResult.contract, "utf-8");
|
| 63 |
writeFileSync(resolve(outputDir, "README.md"), requirements, "utf-8");
|
| 64 |
|
| 65 |
-
|
| 66 |
const auditorResult = await auditorAgent.invoke({ repoPath: outputDir });
|
| 67 |
-
|
| 68 |
for (const f of auditorResult.findings) {
|
| 69 |
-
|
| 70 |
}
|
| 71 |
|
| 72 |
await send(
|
|
@@ -77,14 +78,14 @@ app.post("/api/run", (c) => {
|
|
| 77 |
);
|
| 78 |
|
| 79 |
// === TESTER ===
|
| 80 |
-
|
| 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 |
-
|
| 87 |
-
|
| 88 |
// Garante que o objeto enviado tem exatamente o que o front espera
|
| 89 |
await send(
|
| 90 |
"tester",
|
|
@@ -96,15 +97,17 @@ app.post("/api/run", (c) => {
|
|
| 96 |
}),
|
| 97 |
);
|
| 98 |
} else {
|
| 99 |
-
|
| 100 |
await send("tester", JSON.stringify({ status: "skipped", iterations: 0 }));
|
| 101 |
}
|
| 102 |
|
| 103 |
-
|
| 104 |
await send("done", "ok");
|
| 105 |
} catch (err) {
|
| 106 |
const message = err instanceof Error ? err.message : String(err);
|
| 107 |
await send("error", message);
|
|
|
|
|
|
|
| 108 |
}
|
| 109 |
});
|
| 110 |
});
|
|
|
|
| 13 |
import { auditorAgent } from "./agents/auditor/agent.ts";
|
| 14 |
import { testerAgent } from "./agents/tester/agent.ts";
|
| 15 |
import { mapFindingToReport } from "./utils/mapFinding.js";
|
| 16 |
+
import { logger, setLogSink, clearLogSink } from "./logger.ts";
|
| 17 |
|
| 18 |
const app = new Hono();
|
| 19 |
|
|
|
|
| 36 |
};
|
| 37 |
|
| 38 |
try {
|
| 39 |
+
setLogSink((msg) => send("log", msg));
|
| 40 |
+
|
| 41 |
// === CODER ===
|
| 42 |
+
logger.info("[Coder] Gerando smart contract a partir dos requisitos...");
|
| 43 |
const coderResult = await coderAgent.invoke({ requirements: [requirements] });
|
| 44 |
|
|
|
|
|
|
|
| 45 |
if (coderResult.compilationErrors.length > 0) {
|
| 46 |
+
logger.info(`[Coder] Erros de compilação restantes: ${coderResult.compilationErrors.length}`);
|
| 47 |
} else {
|
| 48 |
+
logger.info("[Coder] Contrato compilado sem erros.");
|
| 49 |
}
|
| 50 |
|
| 51 |
await send(
|
|
|
|
| 63 |
writeFileSync(resolve(outputDir, "Contract.sol"), coderResult.contract, "utf-8");
|
| 64 |
writeFileSync(resolve(outputDir, "README.md"), requirements, "utf-8");
|
| 65 |
|
| 66 |
+
logger.info("[Auditor] Iniciando auditoria de segurança...");
|
| 67 |
const auditorResult = await auditorAgent.invoke({ repoPath: outputDir });
|
| 68 |
+
logger.info(`[Auditor] ${auditorResult.findings.length} vulnerabilidade(s) encontrada(s).`);
|
| 69 |
for (const f of auditorResult.findings) {
|
| 70 |
+
logger.info(`[Auditor] [${f.severity.toUpperCase()}] ${f.title} — ${f.location}`);
|
| 71 |
}
|
| 72 |
|
| 73 |
await send(
|
|
|
|
| 78 |
);
|
| 79 |
|
| 80 |
// === TESTER ===
|
| 81 |
+
logger.info("[Tester] Gerando testes de prova de conceito...");
|
| 82 |
|
| 83 |
if (auditorResult.findings.length > 0) {
|
| 84 |
const report = mapFindingToReport(auditorResult.findings[0], coderResult.contract);
|
| 85 |
const testerResult = await testerAgent.invoke({ report });
|
| 86 |
|
| 87 |
+
logger.info(`[Tester] Execução concluída com status: ${testerResult.status}`);
|
| 88 |
+
|
| 89 |
// Garante que o objeto enviado tem exatamente o que o front espera
|
| 90 |
await send(
|
| 91 |
"tester",
|
|
|
|
| 97 |
}),
|
| 98 |
);
|
| 99 |
} else {
|
| 100 |
+
logger.info("[Tester] Nenhuma vulnerabilidade para testar.");
|
| 101 |
await send("tester", JSON.stringify({ status: "skipped", iterations: 0 }));
|
| 102 |
}
|
| 103 |
|
| 104 |
+
logger.info("Pipeline concluído.");
|
| 105 |
await send("done", "ok");
|
| 106 |
} catch (err) {
|
| 107 |
const message = err instanceof Error ? err.message : String(err);
|
| 108 |
await send("error", message);
|
| 109 |
+
} finally {
|
| 110 |
+
clearLogSink();
|
| 111 |
}
|
| 112 |
});
|
| 113 |
});
|