Spaces:
Runtime error
Runtime error
Uanderson Silva commited on
Commit ·
2c8beb8
1
Parent(s): 004e1f0
improve reflection loop and add cache
Browse files- src/agents/auditor/agent.ts +37 -27
- src/agents/auditor/prompts.ts +24 -6
- src/agents/auditor/utils.ts +46 -0
src/agents/auditor/agent.ts
CHANGED
|
@@ -24,11 +24,12 @@ import {
|
|
| 24 |
GATHER_CONTEXT_PROMPT,
|
| 25 |
JUDGE_FINDINGS_PROMPT,
|
| 26 |
RANK_FILES_PROMPT,
|
|
|
|
| 27 |
} from "./prompts.ts";
|
| 28 |
import { AuditorState, CandidateFindingSchema, FileRankingSchema, JudgeReviewSchema } from "./state.ts";
|
| 29 |
import { buildRepoTree } from "./tools/repo-tree/tool.ts";
|
| 30 |
import { analyzeSolidityFile } from "./tools/solidity-analyzer/tool.ts";
|
| 31 |
-
import { matchLines } from "./utils.ts";
|
| 32 |
|
| 33 |
const llmHaiku = createLLM("anthropic", { model: "claude-haiku-4-5", maxTokens: 20000 });
|
| 34 |
const llmOpus = createLLM("anthropic", { model: "claude-opus-4-8", temperature: null, maxTokens: 20000 });
|
|
@@ -85,9 +86,9 @@ const defineScope: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 85 |
const rankingModel = llmHaiku.withStructuredOutput(RankFilesSchema);
|
| 86 |
|
| 87 |
const { rankings } = await rankingModel.invoke([
|
| 88 |
-
new SystemMessage(RANK_FILES_PROMPT),
|
| 89 |
new HumanMessage(
|
| 90 |
-
`
|
| 91 |
),
|
| 92 |
]);
|
| 93 |
|
|
@@ -136,21 +137,24 @@ const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 136 |
const parts: string[] = [];
|
| 137 |
|
| 138 |
if (docEntries.length > 0) {
|
| 139 |
-
parts.push("##
|
| 140 |
for (const { filePath, content } of docEntries) {
|
| 141 |
parts.push(`### ${filePath}\n${content}`);
|
| 142 |
}
|
| 143 |
}
|
| 144 |
|
| 145 |
-
parts.push(`##
|
| 146 |
|
| 147 |
-
parts.push("##
|
| 148 |
for (const { analysis } of solidityEntries) {
|
| 149 |
parts.push(analysis);
|
| 150 |
}
|
| 151 |
|
| 152 |
const model = llmHaiku.withStructuredOutput(z.object({ context: z.string() }));
|
| 153 |
-
const result = await model.invoke([
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
const fileTreeBlock = `## Árvore de Arquivos\n\n\`\`\`\n${state.fileTree}\n\`\`\``;
|
| 156 |
const structuralBlock = `## Análise Estrutural dos Contratos\n\n${solidityEntries.map(({ analysis }) => analysis).join("\n\n---\n\n")}`;
|
|
@@ -166,20 +170,14 @@ const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 166 |
const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
|
| 167 |
const model = llmOpus.withStructuredOutput(z.object({ findings: z.array(CandidateFindingSchema) }));
|
| 168 |
|
| 169 |
-
const
|
| 170 |
-
state.judgeReviews.length > 0
|
| 171 |
-
? state.judgeReviews
|
| 172 |
-
.map((r, i) => {
|
| 173 |
-
const title = state.candidateFindings[i]?.title ?? `Finding ${i + 1}`;
|
| 174 |
-
return `- "${title}": ${r.isFalsePositive ? "FALSE POSITIVE" : "TRUE POSITIVE"}\n Judge: ${r.review}`;
|
| 175 |
-
})
|
| 176 |
-
.join("\n")
|
| 177 |
-
: null;
|
| 178 |
|
| 179 |
logger.info(
|
| 180 |
`findVulnerabilities: invoking LLM for ${state.scope.length} file(s) in parallel (iteration ${state.reflectionCount + 1})`,
|
| 181 |
);
|
| 182 |
|
|
|
|
|
|
|
| 183 |
const allFindings = await Promise.all(
|
| 184 |
state.scope.map(async (filePath) => {
|
| 185 |
let source: string;
|
|
@@ -190,16 +188,23 @@ const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 190 |
}
|
| 191 |
if (!source) return [];
|
| 192 |
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
|
| 198 |
logger.debug(`findVulnerabilities: processing ${filePath}`);
|
| 199 |
|
| 200 |
const result = await model.invoke([
|
| 201 |
-
new SystemMessage(
|
| 202 |
-
new HumanMessage(
|
| 203 |
]);
|
| 204 |
|
| 205 |
return result.findings.map((finding: any) => ({
|
|
@@ -231,6 +236,8 @@ const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 231 |
|
| 232 |
logger.info(`judgeFindings: reviewing ${state.candidateFindings.length} candidate finding(s) in parallel`);
|
| 233 |
|
|
|
|
|
|
|
| 234 |
const reviews = await Promise.all(
|
| 235 |
state.candidateFindings.map(async (finding, i) => {
|
| 236 |
let source: string;
|
|
@@ -240,14 +247,17 @@ const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 240 |
source = "";
|
| 241 |
}
|
| 242 |
|
| 243 |
-
const findingText = `[
|
| 244 |
|
| 245 |
logger.debug(`judgeFindings: reviewing finding ${i + 1}: ${finding.title}`);
|
| 246 |
return model.invoke([
|
| 247 |
-
new SystemMessage(JUDGE_FINDINGS_PROMPT),
|
| 248 |
-
new HumanMessage(
|
| 249 |
-
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
| 251 |
]);
|
| 252 |
}),
|
| 253 |
);
|
|
|
|
| 24 |
GATHER_CONTEXT_PROMPT,
|
| 25 |
JUDGE_FINDINGS_PROMPT,
|
| 26 |
RANK_FILES_PROMPT,
|
| 27 |
+
REFINE_VULNERABILITIES_PROMPT,
|
| 28 |
} from "./prompts.ts";
|
| 29 |
import { AuditorState, CandidateFindingSchema, FileRankingSchema, JudgeReviewSchema } from "./state.ts";
|
| 30 |
import { buildRepoTree } from "./tools/repo-tree/tool.ts";
|
| 31 |
import { analyzeSolidityFile } from "./tools/solidity-analyzer/tool.ts";
|
| 32 |
+
import { buildReviewBlocks, matchLines } from "./utils.ts";
|
| 33 |
|
| 34 |
const llmHaiku = createLLM("anthropic", { model: "claude-haiku-4-5", maxTokens: 20000 });
|
| 35 |
const llmOpus = createLLM("anthropic", { model: "claude-opus-4-8", temperature: null, maxTokens: 20000 });
|
|
|
|
| 86 |
const rankingModel = llmHaiku.withStructuredOutput(RankFilesSchema);
|
| 87 |
|
| 88 |
const { rankings } = await rankingModel.invoke([
|
| 89 |
+
new SystemMessage({ content: [{ type: "text", text: RANK_FILES_PROMPT, cache_control: { type: "ephemeral" } }] }),
|
| 90 |
new HumanMessage(
|
| 91 |
+
`Árvore de arquivos:\n\`\`\`\n${fileTree}\n\`\`\`\n\nArquivos Solidity para classificar:\n${solFiles.map((f) => `- ${f}`).join("\n")}`,
|
| 92 |
),
|
| 93 |
]);
|
| 94 |
|
|
|
|
| 137 |
const parts: string[] = [];
|
| 138 |
|
| 139 |
if (docEntries.length > 0) {
|
| 140 |
+
parts.push("## Documentação\n");
|
| 141 |
for (const { filePath, content } of docEntries) {
|
| 142 |
parts.push(`### ${filePath}\n${content}`);
|
| 143 |
}
|
| 144 |
}
|
| 145 |
|
| 146 |
+
parts.push(`## Árvore de Arquivos\n\n\`\`\`\n${state.fileTree}\n\`\`\``);
|
| 147 |
|
| 148 |
+
parts.push("## Análise Estrutural\n");
|
| 149 |
for (const { analysis } of solidityEntries) {
|
| 150 |
parts.push(analysis);
|
| 151 |
}
|
| 152 |
|
| 153 |
const model = llmHaiku.withStructuredOutput(z.object({ context: z.string() }));
|
| 154 |
+
const result = await model.invoke([
|
| 155 |
+
new SystemMessage({ content: [{ type: "text", text: GATHER_CONTEXT_PROMPT, cache_control: { type: "ephemeral" } }] }),
|
| 156 |
+
new HumanMessage(parts.join("\n\n")),
|
| 157 |
+
]);
|
| 158 |
|
| 159 |
const fileTreeBlock = `## Árvore de Arquivos\n\n\`\`\`\n${state.fileTree}\n\`\`\``;
|
| 160 |
const structuralBlock = `## Análise Estrutural dos Contratos\n\n${solidityEntries.map(({ analysis }) => analysis).join("\n\n---\n\n")}`;
|
|
|
|
| 170 |
const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
|
| 171 |
const model = llmOpus.withStructuredOutput(z.object({ findings: z.array(CandidateFindingSchema) }));
|
| 172 |
|
| 173 |
+
const isReflection = state.judgeReviews.length > 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
logger.info(
|
| 176 |
`findVulnerabilities: invoking LLM for ${state.scope.length} file(s) in parallel (iteration ${state.reflectionCount + 1})`,
|
| 177 |
);
|
| 178 |
|
| 179 |
+
const cachedContext = { type: "text" as const, text: `Contexto do Protocolo:\n${state.repoContext}`, cache_control: { type: "ephemeral" as const } };
|
| 180 |
+
|
| 181 |
const allFindings = await Promise.all(
|
| 182 |
state.scope.map(async (filePath) => {
|
| 183 |
let source: string;
|
|
|
|
| 188 |
}
|
| 189 |
if (!source) return [];
|
| 190 |
|
| 191 |
+
const fileEntries = isReflection
|
| 192 |
+
? state.candidateFindings
|
| 193 |
+
.map((f, i) => ({ finding: f, review: state.judgeReviews[i] }))
|
| 194 |
+
.filter(({ finding }) => finding.path === filePath)
|
| 195 |
+
: [];
|
| 196 |
+
|
| 197 |
+
const isRefinement = fileEntries.length > 0;
|
| 198 |
+
const promptText = isRefinement ? REFINE_VULNERABILITIES_PROMPT : FIND_VULNERABILITIES_PROMPT;
|
| 199 |
+
const contractText = isRefinement
|
| 200 |
+
? `Contrato (${filePath}):\n\n${source}\n\n${buildReviewBlocks(fileEntries, state.reflectionCount)}`
|
| 201 |
+
: `Contrato (${filePath}):\n\n${source}`;
|
| 202 |
|
| 203 |
logger.debug(`findVulnerabilities: processing ${filePath}`);
|
| 204 |
|
| 205 |
const result = await model.invoke([
|
| 206 |
+
new SystemMessage({ content: [{ type: "text", text: promptText, cache_control: { type: "ephemeral" } }] }),
|
| 207 |
+
new HumanMessage({ content: [cachedContext, { type: "text", text: contractText }] }),
|
| 208 |
]);
|
| 209 |
|
| 210 |
return result.findings.map((finding: any) => ({
|
|
|
|
| 236 |
|
| 237 |
logger.info(`judgeFindings: reviewing ${state.candidateFindings.length} candidate finding(s) in parallel`);
|
| 238 |
|
| 239 |
+
const cachedContext = { type: "text" as const, text: `Contexto do Protocolo:\n${state.repoContext}`, cache_control: { type: "ephemeral" as const } };
|
| 240 |
+
|
| 241 |
const reviews = await Promise.all(
|
| 242 |
state.candidateFindings.map(async (finding, i) => {
|
| 243 |
let source: string;
|
|
|
|
| 247 |
source = "";
|
| 248 |
}
|
| 249 |
|
| 250 |
+
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\`\`\``;
|
| 251 |
|
| 252 |
logger.debug(`judgeFindings: reviewing finding ${i + 1}: ${finding.title}`);
|
| 253 |
return model.invoke([
|
| 254 |
+
new SystemMessage({ content: [{ type: "text", text: JUDGE_FINDINGS_PROMPT, cache_control: { type: "ephemeral" } }] }),
|
| 255 |
+
new HumanMessage({
|
| 256 |
+
content: [
|
| 257 |
+
cachedContext,
|
| 258 |
+
{ type: "text", text: `Contrato (${finding.path}):\n\n${source}\n\nAchado para Revisão:\n\n${findingText}` },
|
| 259 |
+
],
|
| 260 |
+
}),
|
| 261 |
]);
|
| 262 |
}),
|
| 263 |
);
|
src/agents/auditor/prompts.ts
CHANGED
|
@@ -80,14 +80,32 @@ Para cada vulnerabilidade encontrada, forneça OBRIGATORIAMENTE todos os campos
|
|
| 80 |
|
| 81 |
## Regra de completude
|
| 82 |
|
| 83 |
-
Reporte vulnerabilidades mesmo que não sejam imediatamente exploráveis. Vulnerabilidades podem ser de segurança, inconsistências de design, desalinhamentos econômicos ou falhas de observabilidade. Se nenhuma vulnerabilidade for encontrada, retorne um array vazio.
|
| 84 |
|
| 85 |
-
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
export const JUDGE_FINDINGS_PROMPT = `Você é um revisor rigoroso de segurança de smart contracts com profundo conhecimento em Solidity, execução EVM e design de protocolos. Avalie cada vulnerabilidade candidata submetida pelo auditor e determine se é um verdadeiro positivo ou um falso positivo.
|
| 93 |
|
|
|
|
| 80 |
|
| 81 |
## Regra de completude
|
| 82 |
|
| 83 |
+
Reporte vulnerabilidades mesmo que não sejam imediatamente exploráveis. Vulnerabilidades podem ser de segurança, inconsistências de design, desalinhamentos econômicos ou falhas de observabilidade. Se nenhuma vulnerabilidade for encontrada, retorne um array vazio.`;
|
| 84 |
|
| 85 |
+
export const REFINE_VULNERABILITIES_PROMPT = `Você é um auditor sênior de segurança de smart contracts refinando seus próprios achados com base no feedback de um revisor especialista independente.
|
| 86 |
|
| 87 |
+
Na iteração anterior, você analisou um contrato Solidity e gerou uma lista de vulnerabilidades candidatas. Um revisor especialista avaliou cada achado e forneceu: veredicto (verdadeiro ou falso positivo), análise técnica detalhada, nível de confiança e caminhos de exploit ou razões de bloqueio.
|
| 88 |
+
|
| 89 |
+
**Sua tarefa**: produzir uma lista final e refinada de vulnerabilidades incorporando o feedback do revisor.
|
| 90 |
+
|
| 91 |
+
## Regras de refinamento
|
| 92 |
+
|
| 93 |
+
1. **Falso positivo com confiança ≥ 80%**: remova o achado sem exceção.
|
| 94 |
+
2. **Falso positivo com confiança < 80%**: reavalie com base na análise do revisor. Mantenha apenas se encontrar evidência nova ou argumento técnico que o revisor não considerou — e reflita isso na descrição.
|
| 95 |
+
3. **Verdadeiro positivo**: mantenha o achado. Incorpore melhorias sugeridas pelo revisor (descrição mais precisa, snippet mais completo, recomendação mais específica, caminhos de exploit detalhados).
|
| 96 |
+
4. **Novos achados**: se o revisor apontou superfícies de ataque não cobertas em seus achados originais, investigue o código-fonte e adicione novos achados para elas.
|
| 97 |
+
5. Não adicione achados que não sejam suportados pelo código-fonte ou pelo feedback do revisor.
|
| 98 |
+
|
| 99 |
+
## Formato de saída
|
| 100 |
+
|
| 101 |
+
Idêntico ao da análise inicial. Para cada vulnerabilidade:
|
| 102 |
+
- **title**: nome curto e preciso
|
| 103 |
+
- **description**: (a) comportamento esperado, (b) comportamento observado, (c) impacto concreto — mínimo 3 frases, máximo 5
|
| 104 |
+
- **recommendation**: correção específica e acionável com referência ao padrão correto
|
| 105 |
+
- **severity**: \`"high"\` / \`"medium"\` / \`"low"\`
|
| 106 |
+
- **codeSnippet**: trecho exato e completo copiado literalmente do código-fonte, sem omissões, reticências ou pseudocódigo
|
| 107 |
+
|
| 108 |
+
Se nenhuma vulnerabilidade restar após o refinamento, retorne um array vazio.`;
|
| 109 |
|
| 110 |
export const JUDGE_FINDINGS_PROMPT = `Você é um revisor rigoroso de segurança de smart contracts com profundo conhecimento em Solidity, execução EVM e design de protocolos. Avalie cada vulnerabilidade candidata submetida pelo auditor e determine se é um verdadeiro positivo ou um falso positivo.
|
| 111 |
|
src/agents/auditor/utils.ts
CHANGED
|
@@ -34,3 +34,49 @@ export const matchLines = (fileContent: string, codeSnippet: string): string | n
|
|
| 34 |
|
| 35 |
return null;
|
| 36 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
return null;
|
| 36 |
};
|
| 37 |
+
|
| 38 |
+
type ReviewEntry = {
|
| 39 |
+
finding: {
|
| 40 |
+
title: string;
|
| 41 |
+
severity: string;
|
| 42 |
+
location: string;
|
| 43 |
+
description: string;
|
| 44 |
+
codeSnippet: string;
|
| 45 |
+
};
|
| 46 |
+
review: {
|
| 47 |
+
isFalsePositive: boolean;
|
| 48 |
+
confidence: number;
|
| 49 |
+
review: string;
|
| 50 |
+
exploitablePaths: string[];
|
| 51 |
+
};
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
export const buildReviewBlocks = (fileEntries: ReviewEntry[], iterationCount: number): string => {
|
| 55 |
+
const blocks = fileEntries.map(({ finding, review }, i) => {
|
| 56 |
+
const verdict = review.isFalsePositive
|
| 57 |
+
? `FALSO POSITIVO (confiança: ${review.confidence}/100)`
|
| 58 |
+
: `VERDADEIRO POSITIVO (confiança: ${review.confidence}/100)`;
|
| 59 |
+
const pathsLabel = review.isFalsePositive ? "Razão de Bloqueio" : "Caminhos de Exploração";
|
| 60 |
+
const pathsContent =
|
| 61 |
+
review.exploitablePaths.length > 0
|
| 62 |
+
? review.exploitablePaths.map((p) => ` - ${p}`).join("\n")
|
| 63 |
+
: " (nenhum fornecido)";
|
| 64 |
+
|
| 65 |
+
return `[Achado ${i + 1}/${fileEntries.length}] ${finding.title}
|
| 66 |
+
Severidade: ${finding.severity}
|
| 67 |
+
Localização: linhas ${finding.location}
|
| 68 |
+
Descrição: ${finding.description}
|
| 69 |
+
|
| 70 |
+
Código:
|
| 71 |
+
\`\`\`solidity
|
| 72 |
+
${finding.codeSnippet}
|
| 73 |
+
\`\`\`
|
| 74 |
+
|
| 75 |
+
Veredito do Revisor: ${verdict}
|
| 76 |
+
Análise do Revisor: ${review.review}
|
| 77 |
+
${pathsLabel}:
|
| 78 |
+
${pathsContent}`;
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
return `=== Iteração ${iterationCount} — Achados e Revisões do Especialista (${fileEntries.length} achado(s)) ===\n\n${blocks.join("\n\n---\n\n")}`;
|
| 82 |
+
};
|