Spaces:
Runtime error
Runtime error
File size: 12,387 Bytes
10be466 4c8dfec b6cd280 4c8dfec b6cd280 32a4f79 42ebf1f 184b6e6 10be466 0b3cd21 2c8beb8 0b3cd21 184b6e6 10be466 f27a135 b82e2a6 f27a135 b6cd280 10be466 fc99222 48c7a9b 48a8015 10be466 52c3560 42ebf1f 48c7a9b 48a8015 fc99222 48a8015 42ebf1f 0b3cd21 f27a135 0b3cd21 2c8beb8 0b3cd21 2c8beb8 0b3cd21 42ebf1f 0b3cd21 004e1f0 42ebf1f 004e1f0 10be466 b6cd280 4c8dfec fc99222 42ebf1f 48a8015 10be466 4c8dfec 10be466 0b3cd21 10be466 4c8dfec 10be466 4c8dfec 10be466 2c8beb8 10be466 2c8beb8 10be466 2c8beb8 0b3cd21 10be466 f27a135 2c8beb8 184b6e6 2c8beb8 48a8015 004e1f0 48a8015 48c7a9b 42ebf1f 10be466 fc99222 10be466 004e1f0 4c8dfec f27a135 881b356 2c8beb8 881b356 48c7a9b 881b356 fc99222 881b356 184b6e6 42ebf1f 184b6e6 4c8dfec 42ebf1f 48c7a9b 48a8015 fc99222 881b356 4c8dfec 48a8015 fc99222 4c8dfec 48c7a9b fc99222 4c8dfec 48a8015 4c8dfec f27a135 881b356 42ebf1f 881b356 184b6e6 4c8dfec 184b6e6 4c8dfec 184b6e6 42ebf1f 184b6e6 48a8015 881b356 4c8dfec 881b356 4c8dfec 881b356 4c8dfec 48c7a9b 48a8015 fc99222 4c8dfec 881b356 4c8dfec b6cd280 10be466 4c8dfec 48a8015 10be466 4c8dfec 48a8015 4c8dfec b6cd280 48a8015 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 | import fs from "node:fs";
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
import { END, type GraphNode, START, StateGraph } from "@langchain/langgraph";
import { z } from "zod";
import { createLLM } from "../../config/llm.ts";
import { emitStep, logger } from "../../logger.ts";
import { MAX_DOC_CHARS, MAX_REFLECTIONS, MAX_SOL_CHARS, MIN_FILE_IMPORTANCE } from "./config.ts";
import {
FIND_VULNERABILITIES_PROMPT,
GATHER_CONTEXT_PROMPT,
JUDGE_FINDINGS_PROMPT,
RANK_FILES_PROMPT,
REFINE_VULNERABILITIES_PROMPT,
} from "./prompts.ts";
import { AuditorState, CandidateFindingSchema, FileRankingSchema, JudgeReviewSchema } from "./state.ts";
import { buildRepoTree } from "./tools/repo-tree/tool.ts";
import { analyzeSolidityFile } from "./tools/solidity-analyzer/tool.ts";
import { buildReviewBlocks, matchLines, walkDirectory } from "./utils.ts";
const llmHaiku = createLLM("anthropic", { model: "claude-haiku-4-5", maxTokens: 20000 });
const llmOpus = createLLM("anthropic", { model: "claude-opus-4-8", temperature: null, maxTokens: 20000 });
const llmSonnet = createLLM("anthropic", { model: "claude-sonnet-4-6", maxTokens: 20000 });
const defineScope: GraphNode<typeof AuditorState> = async (state) => {
emitStep({ agent: "auditor", step: "scope", status: "running" });
logger.info(`[Auditor] defineScope: percorrendo repositório em ${state.repoPath}`);
const solFiles: string[] = [];
const docFiles: string[] = [];
walkDirectory(state.repoPath, 0, solFiles, docFiles);
const fileTree = buildRepoTree(state.repoPath);
logger.info(
`[Auditor] defineScope: encontrado(s) ${solFiles.length} arquivo(s) Solidity e ${docFiles.length} arquivo(s) de documentação`,
);
logger.debug(`[Auditor] defineScope: arquivos Solidity: ${JSON.stringify(solFiles)}`);
logger.debug(`[Auditor] defineScope: arquivos de documentação: ${JSON.stringify(docFiles)}`);
logger.debug(`[Auditor] defineScope: árvore de arquivos:\n${fileTree}`);
emitStep({ agent: "auditor", step: "scope", status: "done" });
logger.info("[Auditor] defineScope: rankeando arquivos por importância");
const RankFilesSchema = z.object({ rankings: z.array(FileRankingSchema) });
const rankingModel = llmHaiku.withStructuredOutput(RankFilesSchema);
const { rankings } = await rankingModel.invoke([
new SystemMessage({ content: [{ type: "text", text: RANK_FILES_PROMPT, cache_control: { type: "ephemeral" } }] }),
new HumanMessage(
`Árvore de arquivos:\n\`\`\`\n${fileTree}\n\`\`\`\n\nArquivos Solidity para classificar:\n${solFiles.map((f) => `- ${f}`).join("\n")}`,
),
]);
const sorted = [...rankings].sort((a, b) => b.importance - a.importance);
logger.info(
`[Auditor] defineScope: rankings:\n${sorted.map((r) => ` [${r.importance}/5] ${r.filePath} — ${r.reasoning}`).join("\n")}`,
);
const importantFiles = sorted.filter((r) => r.importance >= MIN_FILE_IMPORTANCE).map((r) => r.filePath);
const skipped = solFiles.length - importantFiles.length;
if (skipped > 0) {
logger.info(
`[Auditor] defineScope: pulando ${skipped} arquivo(s) de baixa importância (importância < ${MIN_FILE_IMPORTANCE})`,
);
}
return { scope: importantFiles, docs: docFiles, fileTree, fileRankings: sorted };
};
const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
emitStep({ agent: "auditor", step: "ctx", status: "running" });
logger.info(
`[Auditor] gatherContext: processando ${state.scope.length} arquivo(s) Solidity e ${state.docs.length} arquivo(s) de documentação`,
);
const readFile = (filePath: string): string => {
try {
return fs.readFileSync(filePath, "utf-8");
} catch {
return "";
}
};
const solidityEntries: { filePath: string; source: string; analysis: string }[] = [];
for (const filePath of state.scope) {
const source = readFile(filePath).slice(0, MAX_SOL_CHARS);
if (!source) continue;
const ranking = state.fileRankings.find((r) => r.filePath === filePath);
const mode = ranking && ranking.importance >= 4 ? "full" : "short";
const analysis = await analyzeSolidityFile(source, mode, filePath, ranking?.importance);
solidityEntries.push({ filePath, source, analysis });
}
const docEntries: { filePath: string; content: string }[] = [];
for (const filePath of state.docs) {
const content = readFile(filePath).slice(0, MAX_DOC_CHARS);
if (content) docEntries.push({ filePath, content });
}
const parts: string[] = [];
if (docEntries.length > 0) {
parts.push("## Documentação\n");
for (const { filePath, content } of docEntries) {
parts.push(`### ${filePath}\n${content}`);
}
}
parts.push(`## Árvore de Arquivos\n\n\`\`\`\n${state.fileTree}\n\`\`\``);
parts.push("## Análise Estrutural\n");
for (const { analysis } of solidityEntries) {
parts.push(analysis);
}
const model = llmHaiku.withStructuredOutput(z.object({ context: z.string() }));
const result = await model.invoke([
new SystemMessage({
content: [{ type: "text", text: GATHER_CONTEXT_PROMPT, cache_control: { type: "ephemeral" } }],
}),
new HumanMessage(parts.join("\n\n")),
]);
const fileTreeBlock = `## Árvore de Arquivos\n\n\`\`\`\n${state.fileTree}\n\`\`\``;
const structuralBlock = `## Análise Estrutural dos Contratos\n\n${solidityEntries.map(({ analysis }) => analysis).join("\n\n---\n\n")}`;
const repoContext = [result.context, fileTreeBlock, structuralBlock].join("\n\n");
logger.debug(`[Auditor] gatherContext: contexto completo:\n${parts.join("\n\n")}`);
logger.info(`[Auditor] gatherContext: contexto construído (${repoContext.length} caracteres)`);
logger.debug(`[Auditor] gatherContext: contexto compactado:\n${repoContext}`);
emitStep({ agent: "auditor", step: "ctx", status: "done" });
return { repoContext };
};
const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
const model = llmOpus.withStructuredOutput(z.object({ findings: z.array(CandidateFindingSchema) }));
const isReflection = state.judgeReviews.length > 0;
logger.info(
`[Auditor] findVulnerabilities: invocando LLM para ${state.scope.length} arquivo(s) em paralelo (iteração ${state.reflectionCount + 1})`,
);
emitStep({ agent: "auditor", step: "find", status: "running", detail: `iter ${state.reflectionCount + 1}` });
const cachedContext = {
type: "text" as const,
text: `Contexto do Protocolo:\n${state.repoContext}`,
cache_control: { type: "ephemeral" as const },
};
const processFile = async (filePath: string) => {
let source: string;
try {
source = fs.readFileSync(filePath, "utf-8").slice(0, MAX_SOL_CHARS);
} catch {
return [];
}
if (!source) return [];
const fileEntries = isReflection
? state.candidateFindings
.map((f, i) => ({ finding: f, review: state.judgeReviews[i] }))
.filter(({ finding }) => finding.path === filePath)
: [];
const isRefinement = fileEntries.length > 0;
const promptText = isRefinement ? REFINE_VULNERABILITIES_PROMPT : FIND_VULNERABILITIES_PROMPT;
const contractText = isRefinement
? `Contrato (${filePath}):\n\n${source}\n\n${buildReviewBlocks(fileEntries, state.reflectionCount)}`
: `Contrato (${filePath}):\n\n${source}`;
logger.debug(`[Auditor] findVulnerabilities: processando ${filePath}`);
const result = await model.invoke([
new SystemMessage({ content: [{ type: "text", text: promptText, cache_control: { type: "ephemeral" } }] }),
new HumanMessage({ content: [cachedContext, { type: "text", text: contractText }] }),
]);
return result.findings.map((finding: any) => ({
...finding,
path: filePath,
location: matchLines(source, finding.codeSnippet) ?? "",
}));
};
const [firstFile, ...restFiles] = state.scope;
const firstFindings = firstFile ? await processFile(firstFile) : [];
const restFindings = await Promise.all(restFiles.map(processFile));
const candidateFindings = [firstFindings, ...restFindings].flat();
logger.info(
`[Auditor] findVulnerabilities: LLM retornou ${candidateFindings.length} finding(s) candidato(s) no total`,
);
logger.debug(`[Auditor] findVulnerabilities: findings:\n${JSON.stringify(candidateFindings, null, 2)}`);
emitStep({ agent: "auditor", step: "find", status: "done" });
return { candidateFindings };
};
const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
emitStep({ agent: "auditor", step: "judge", status: "running" });
if (state.candidateFindings.length === 0) {
logger.info("[Auditor] judgeFindings: sem findings candidatos para revisar, pulando chamada ao LLM");
emitStep({ agent: "auditor", step: "judge", status: "done" });
return {
judgeReviews: [],
findings: [],
reflectionCount: state.reflectionCount + 1,
};
}
const model = llmSonnet.withStructuredOutput(JudgeReviewSchema);
logger.info(
`[Auditor] judgeFindings: revisando ${state.candidateFindings.length} finding(s) candidato(s) em paralelo`,
);
const cachedContext = {
type: "text" as const,
text: `Contexto do Protocolo:\n${state.repoContext}`,
cache_control: { type: "ephemeral" as const },
};
const reviewFinding = async (finding: (typeof state.candidateFindings)[number], i: number) => {
let source: string;
try {
source = fs.readFileSync(finding.path, "utf-8").slice(0, MAX_SOL_CHARS);
} catch {
source = "";
}
const findingText = `[Achado ${i + 1}] ${finding.title}\nSeveridade: ${finding.severity}\nDescrição: ${finding.description}\nLocalização: ${finding.path} linhas ${finding.location}\nCódigo:\n\`\`\`solidity\n${finding.codeSnippet}\n\`\`\``;
logger.debug(`[Auditor] judgeFindings: revisando finding ${i + 1}: ${finding.title}`);
return model.invoke([
new SystemMessage({
content: [{ type: "text", text: JUDGE_FINDINGS_PROMPT, cache_control: { type: "ephemeral" } }],
}),
new HumanMessage({
content: [
cachedContext,
{ type: "text", text: `Contrato (${finding.path}):\n\n${source}\n\nAchado para Revisão:\n\n${findingText}` },
],
}),
]);
};
const [firstFinding, ...restFindings] = state.candidateFindings;
const firstReview = await reviewFinding(firstFinding, 0);
const restReviews = await Promise.all(restFindings.map((f, i) => reviewFinding(f, i + 1)));
const reviews = [firstReview, ...restReviews];
const confirmedEntries = state.candidateFindings
.map((finding, i) => ({ finding, review: reviews[i] }))
.filter(({ review }) => !review.isFalsePositive);
const findings = confirmedEntries.map(({ finding, review }) => ({
...finding,
judgeReview: {
review: review.review,
confidence: review.confidence,
exploitablePaths: review.exploitablePaths,
},
}));
const falsePositiveCount = state.candidateFindings.length - findings.length;
logger.info(`[Auditor] judgeFindings: ${findings.length} confirmado(s), ${falsePositiveCount} falso(s) positivo(s)`);
logger.debug(`[Auditor] judgeFindings: revisões:\n${JSON.stringify(reviews, null, 2)}`);
emitStep({ agent: "auditor", step: "judge", status: "done" });
return {
judgeReviews: reviews,
findings,
reflectionCount: state.reflectionCount + 1,
};
};
export const auditorAgent = new StateGraph(AuditorState)
.addNode("defineScope", defineScope)
.addNode("gatherContext", gatherContext)
.addNode("findVulnerabilities", findVulnerabilities)
.addNode("judgeFindings", judgeFindings)
.addEdge(START, "defineScope")
.addEdge("defineScope", "gatherContext")
.addEdge("gatherContext", "findVulnerabilities")
.addEdge("findVulnerabilities", "judgeFindings")
.addConditionalEdges("judgeFindings", (state) => {
const hasFalsePositives = state.judgeReviews.some((r) => r.isFalsePositive);
if (hasFalsePositives && state.reflectionCount < MAX_REFLECTIONS) {
return "findVulnerabilities";
}
return END;
})
.compile();
export const testAgent = new StateGraph(AuditorState)
.addNode("defineScope", defineScope)
.addNode("gatherContext", gatherContext)
.addEdge(START, "defineScope")
.addEdge("defineScope", "gatherContext")
.addEdge("gatherContext", END)
.compile();
|