Uanderson Ricardo commited on
Commit
3ada692
·
2 Parent(s): 61b52c0184b6e6

Merge pull request #6 from uandersonricardo/improve-audit-agent

Browse files
biome.json CHANGED
@@ -1,55 +1,53 @@
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
3
- "vcs": {
4
- "enabled": true,
5
- "clientKind": "git",
6
- "useIgnoreFile": true
7
- },
8
- "files": {
9
- "includes": [
10
- "**",
11
- "!!**/dist"
12
- ]
13
- },
14
- "formatter": {
15
- "enabled": true,
16
- "lineWidth": 120,
17
- "indentStyle": "space"
18
- },
19
- "linter": {
20
- "enabled": true,
21
- "rules": {
22
- "recommended": true,
23
- "suspicious": {
24
- "noUnknownAtRules": "off",
25
- "noArrayIndexKey": "off",
26
- "noExplicitAny": "off"
27
- },
28
- "complexity": {
29
- "noStaticOnlyClass": "off"
30
- },
31
- "correctness": {
32
- "useExhaustiveDependencies": "off",
33
- "useParseIntRadix": "off"
34
- },
35
- "a11y": "off",
36
- "style": {
37
- "noNonNullAssertion": "off",
38
- "useTemplate": "off"
39
- }
40
- }
41
- },
42
- "javascript": {
43
- "formatter": {
44
- "quoteStyle": "double"
45
- }
46
- },
47
- "assist": {
48
- "enabled": true,
49
- "actions": {
50
- "source": {
51
- "organizeImports": "on"
52
- }
53
- }
54
- }
55
  }
 
1
  {
2
+ "$schema": "https://biomejs.dev/schemas/2.4.14/schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": true
7
+ },
8
+ "files": {
9
+ "includes": ["**", "!!**/dist"]
10
+ },
11
+ "formatter": {
12
+ "enabled": true,
13
+ "lineWidth": 120,
14
+ "indentStyle": "space"
15
+ },
16
+ "linter": {
17
+ "enabled": true,
18
+ "rules": {
19
+ "recommended": true,
20
+ "suspicious": {
21
+ "noUnknownAtRules": "off",
22
+ "noArrayIndexKey": "off",
23
+ "noExplicitAny": "off",
24
+ "noAssignInExpressions": "off"
25
+ },
26
+ "complexity": {
27
+ "noStaticOnlyClass": "off"
28
+ },
29
+ "correctness": {
30
+ "useExhaustiveDependencies": "off",
31
+ "useParseIntRadix": "off"
32
+ },
33
+ "a11y": "off",
34
+ "style": {
35
+ "noNonNullAssertion": "off",
36
+ "useTemplate": "off"
37
+ }
38
+ }
39
+ },
40
+ "javascript": {
41
+ "formatter": {
42
+ "quoteStyle": "double"
43
+ }
44
+ },
45
+ "assist": {
46
+ "enabled": true,
47
+ "actions": {
48
+ "source": {
49
+ "organizeImports": "on"
50
+ }
51
+ }
52
+ }
 
 
53
  }
package-lock.json CHANGED
@@ -11,7 +11,7 @@
11
  "license": "ISC",
12
  "dependencies": {
13
  "@hono/node-server": "^2.0.3",
14
- "@langchain/anthropic": "^1.3.29",
15
  "@langchain/core": "^1.1.45",
16
  "@langchain/google-genai": "^2.1.31",
17
  "@langchain/langgraph": "^1.3.2",
@@ -36,9 +36,9 @@
36
  }
37
  },
38
  "node_modules/@anthropic-ai/sdk": {
39
- "version": "0.95.2",
40
- "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.95.2.tgz",
41
- "integrity": "sha512-Egddwo3sheo1PzUrMkZnH6VkQYwS0h/b/i8vSK8Ta9M45UQipAMeDFH57dYuDAfXMEUUGeKw6CMlremgMZgrSQ==",
42
  "license": "MIT",
43
  "dependencies": {
44
  "json-schema-to-ts": "^3.1.1",
@@ -199,23 +199,25 @@
199
  }
200
  },
201
  "node_modules/@langchain/anthropic": {
202
- "version": "1.4.0",
203
- "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.4.0.tgz",
204
- "integrity": "sha512-rs1yVydrHjyiD31uChdCnKZpmDuKa0Bpz8Raiy9GvqnqmfXPMe0oOrap/2paE+NRSinDbtax8mMpP/yv8EbO1A==",
205
  "license": "MIT",
206
  "dependencies": {
207
- "@anthropic-ai/sdk": "^0.95.1",
208
  "zod": "^3.25.76 || ^4"
209
  },
210
  "engines": {
211
  "node": ">=20"
212
  },
213
  "peerDependencies": {
214
- "@langchain/core": "^1.1.47"
215
  }
216
  },
217
  "node_modules/@langchain/core": {
218
- "version": "1.1.47",
 
 
219
  "license": "MIT",
220
  "dependencies": {
221
  "@cfworker/json-schema": "^4.0.2",
 
11
  "license": "ISC",
12
  "dependencies": {
13
  "@hono/node-server": "^2.0.3",
14
+ "@langchain/anthropic": "^1.4.1",
15
  "@langchain/core": "^1.1.45",
16
  "@langchain/google-genai": "^2.1.31",
17
  "@langchain/langgraph": "^1.3.2",
 
36
  }
37
  },
38
  "node_modules/@anthropic-ai/sdk": {
39
+ "version": "0.103.0",
40
+ "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.103.0.tgz",
41
+ "integrity": "sha512-1uG7RNgoHTUxzOXqSCODKt0UTVlxWiHk/2Tt2/uQJiPW7XzBeKVuJyd3Aw6T3LPyvZV/jDTnPLX7SaM70WLLjA==",
42
  "license": "MIT",
43
  "dependencies": {
44
  "json-schema-to-ts": "^3.1.1",
 
199
  }
200
  },
201
  "node_modules/@langchain/anthropic": {
202
+ "version": "1.4.1",
203
+ "resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.4.1.tgz",
204
+ "integrity": "sha512-h3b6hxThcfh0WdmpuWr+qBi74MN+0BpNI/4H681vwXxbD3hLr2qMYN6ghqcPQhCxGJjg8ufs85qu2/ldSWonYQ==",
205
  "license": "MIT",
206
  "dependencies": {
207
+ "@anthropic-ai/sdk": "^0.103.0",
208
  "zod": "^3.25.76 || ^4"
209
  },
210
  "engines": {
211
  "node": ">=20"
212
  },
213
  "peerDependencies": {
214
+ "@langchain/core": "^1.1.49"
215
  }
216
  },
217
  "node_modules/@langchain/core": {
218
+ "version": "1.1.49",
219
+ "resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.49.tgz",
220
+ "integrity": "sha512-7wkN3Qv/qZqsY0p3h48CNu6E6y5GMYatYxj+JrX4uVNBiqIVQm1Z528QrmayJWVW9SQTQicqRNoyTCzl+K9F8Q==",
221
  "license": "MIT",
222
  "dependencies": {
223
  "@cfworker/json-schema": "^4.0.2",
package.json CHANGED
@@ -34,7 +34,7 @@
34
  },
35
  "dependencies": {
36
  "@hono/node-server": "^2.0.3",
37
- "@langchain/anthropic": "^1.3.29",
38
  "@langchain/core": "^1.1.45",
39
  "@langchain/google-genai": "^2.1.31",
40
  "@langchain/langgraph": "^1.3.2",
 
34
  },
35
  "dependencies": {
36
  "@hono/node-server": "^2.0.3",
37
+ "@langchain/anthropic": "^1.4.1",
38
  "@langchain/core": "^1.1.45",
39
  "@langchain/google-genai": "^2.1.31",
40
  "@langchain/langgraph": "^1.3.2",
src/agents/auditor/agent.ts CHANGED
@@ -1,60 +1,27 @@
1
  import fs from "node:fs";
2
- import path from "node:path";
3
 
4
  import { HumanMessage, SystemMessage } from "@langchain/core/messages";
5
  import { END, type GraphNode, START, StateGraph } from "@langchain/langgraph";
6
  import { z } from "zod";
7
 
8
- import { logger } from "../../logger.ts";
9
  import { createLLM } from "../../config/llm.ts";
10
- import { JUDGE_FINDINGS_PROMPT, FIND_VULNERABILITIES_PROMPT, GATHER_CONTEXT_PROMPT } from "./prompts.ts";
11
- import { AuditorState, JudgeReviewSchema, CandidateFindingSchema } from "./state.ts";
12
- import { analyzeSolidityFile } from "./tools/solidity-analyzer-tool.ts";
13
- import { buildRepoTree } from "./tools/repo-tree-tool.ts";
14
  import {
15
- DOC_BASENAMES,
16
- DOC_EXTS,
17
- MAX_DEPTH,
18
- MAX_DOC_CHARS,
19
- MAX_REFLECTIONS,
20
- MAX_SOL_CHARS,
21
- SKIP_DIRS,
22
- SOL_EXT,
23
- SOL_TEST_SUFFIXES,
24
- } from "./config.ts";
25
- import { matchLines } from "./utils.ts";
26
-
27
- const llm = createLLM();
28
-
29
- const walkDirectory = (dir: string, depth: number, solFiles: string[], docFiles: string[]) => {
30
- if (depth > MAX_DEPTH) return;
31
-
32
- let entries: fs.Dirent[];
33
- try {
34
- entries = fs.readdirSync(dir, { withFileTypes: true });
35
- } catch {
36
- return;
37
- }
38
-
39
- for (const entry of entries) {
40
- if (entry.isDirectory()) {
41
- if (!SKIP_DIRS.has(entry.name)) {
42
- walkDirectory(path.join(dir, entry.name), depth + 1, solFiles, docFiles);
43
- }
44
- } else if (entry.isFile()) {
45
- const fullPath = path.join(dir, entry.name);
46
- const ext = path.extname(entry.name).toLowerCase();
47
- const base = path.basename(entry.name, ext).toLowerCase();
48
-
49
- if (ext === SOL_EXT) {
50
- const isTest = SOL_TEST_SUFFIXES.some((suffix) => entry.name.endsWith(suffix));
51
- if (!isTest) solFiles.push(fullPath);
52
- } else if (DOC_EXTS.has(ext) || DOC_BASENAMES.has(base)) {
53
- docFiles.push(fullPath);
54
- }
55
- }
56
- }
57
- };
58
 
59
  const defineScope: GraphNode<typeof AuditorState> = async (state) => {
60
  logger.info(`defineScope: walking repo at ${state.repoPath}`);
@@ -71,7 +38,30 @@ const defineScope: GraphNode<typeof AuditorState> = async (state) => {
71
  logger.debug(`defineScope: doc files: ${JSON.stringify(docFiles)}`);
72
  logger.debug(`defineScope: file tree:\n${fileTree}`);
73
 
74
- return { scope: solFiles, docs: docFiles, fileTree };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  };
76
 
77
  const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
@@ -85,99 +75,113 @@ const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
85
  }
86
  };
87
 
88
- // Read and analyze each Solidity file
89
  const solidityEntries: { filePath: string; source: string; analysis: string }[] = [];
90
  for (const filePath of state.scope) {
91
  const source = readFile(filePath).slice(0, MAX_SOL_CHARS);
92
  if (!source) continue;
93
- const analysis = await analyzeSolidityFile(source, "full");
 
 
 
94
  solidityEntries.push({ filePath, source, analysis });
95
  }
96
 
97
- // Read documentation files
98
  const docEntries: { filePath: string; content: string }[] = [];
99
  for (const filePath of state.docs) {
100
  const content = readFile(filePath).slice(0, MAX_DOC_CHARS);
101
  if (content) docEntries.push({ filePath, content });
102
  }
103
 
104
- // Build the LLM input
105
  const parts: string[] = [];
106
 
107
  if (docEntries.length > 0) {
108
- parts.push("## Documentation\n");
109
  for (const { filePath, content } of docEntries) {
110
  parts.push(`### ${filePath}\n${content}`);
111
  }
112
  }
113
 
114
- parts.push("## Structural Analysis (auto-generated)\n");
115
- for (const { filePath, analysis } of solidityEntries) {
116
- parts.push(`### ${filePath}\n${analysis}`);
117
- }
118
 
119
- parts.push("## Contract Source Code\n");
120
- for (const { filePath, source } of solidityEntries) {
121
- parts.push(`### ${filePath}\n\`\`\`solidity\n${source}\n\`\`\``);
122
  }
123
 
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: context built (${parts.join("\n\n").length} chars)`);
128
  logger.debug(`gatherContext: full context:\n${parts.join("\n\n")}`);
 
 
129
 
130
- return { repoContext: result.context };
131
  };
132
 
133
  const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
134
- const model = llm.withStructuredOutput(z.object({ findings: z.array(CandidateFindingSchema) }));
135
-
136
- const previousFeedback =
137
- state.judgeReviews.length > 0
138
- ? state.judgeReviews
139
- .map((r, i) => {
140
- const title = state.candidateFindings[i]?.title ?? `Finding ${i + 1}`;
141
- return `- "${title}": ${r.isFalsePositive ? "FALSE POSITIVE" : "TRUE POSITIVE"}\n Judge: ${r.review}`;
142
- })
143
- .join("\n")
144
- : null;
145
 
146
  logger.info(
147
  `findVulnerabilities: invoking LLM for ${state.scope.length} file(s) in parallel (iteration ${state.reflectionCount + 1})`,
148
  );
149
 
150
- const allFindings = await Promise.all(
151
- state.scope.map(async (filePath) => {
152
- let source: string;
153
- try {
154
- source = fs.readFileSync(filePath, "utf-8").slice(0, MAX_SOL_CHARS);
155
- } catch {
156
- return [];
157
- }
158
- if (!source) return [];
159
-
160
- let userMessage = `Contract (${filePath}):\n\n${source}\n\nProtocol Context:\n${state.repoContext}`;
161
- if (previousFeedback) {
162
- userMessage += `\n\nJudge feedback from previous iteration (iteration ${state.reflectionCount}):\n${previousFeedback}\n\nRevise your findings accordingly.`;
163
- }
164
-
165
- logger.debug(`findVulnerabilities: processing ${filePath}`);
166
-
167
- const result = await model.invoke([
168
- new SystemMessage(FIND_VULNERABILITIES_PROMPT),
169
- new HumanMessage(userMessage),
170
- ]);
171
-
172
- return result.findings.map((finding: any) => ({
173
- ...finding,
174
- path: filePath,
175
- location: matchLines(source, finding.codeSnippet) ?? "",
176
- }));
177
- }),
178
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
- const candidateFindings = allFindings.flat();
181
  logger.info(`findVulnerabilities: LLM returned ${candidateFindings.length} total candidate finding(s)`);
182
  logger.debug(`findVulnerabilities: findings:\n${JSON.stringify(candidateFindings, null, 2)}`);
183
 
@@ -194,30 +198,44 @@ const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
194
  };
195
  }
196
 
197
- const model = llm.withStructuredOutput(JudgeReviewSchema);
198
 
199
  logger.info(`judgeFindings: reviewing ${state.candidateFindings.length} candidate finding(s) in parallel`);
200
 
201
- const reviews = await Promise.all(
202
- state.candidateFindings.map(async (finding, i) => {
203
- let source: string;
204
- try {
205
- source = fs.readFileSync(finding.path, "utf-8").slice(0, MAX_SOL_CHARS);
206
- } catch {
207
- source = "";
208
- }
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: reviewing finding ${i + 1}: ${finding.title}`);
213
- return model.invoke([
214
- new SystemMessage(JUDGE_FINDINGS_PROMPT),
215
- new HumanMessage(
216
- `Contract (${finding.path}):\n\n${source}\n\nProtocol Context:\n${state.repoContext}\n\nFinding to Review:\n\n${findingText}`,
217
- ),
218
- ]);
219
- }),
220
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
  const confirmedEntries = state.candidateFindings
223
  .map((finding, i) => ({ finding, review: reviews[i] }))
 
1
  import fs from "node:fs";
 
2
 
3
  import { HumanMessage, SystemMessage } from "@langchain/core/messages";
4
  import { END, type GraphNode, START, StateGraph } from "@langchain/langgraph";
5
  import { z } from "zod";
6
 
 
7
  import { createLLM } from "../../config/llm.ts";
8
+ import { logger } from "../../logger.ts";
9
+ import { MAX_DOC_CHARS, MAX_REFLECTIONS, MAX_SOL_CHARS, MIN_FILE_IMPORTANCE } from "./config.ts";
 
 
10
  import {
11
+ FIND_VULNERABILITIES_PROMPT,
12
+ GATHER_CONTEXT_PROMPT,
13
+ JUDGE_FINDINGS_PROMPT,
14
+ RANK_FILES_PROMPT,
15
+ REFINE_VULNERABILITIES_PROMPT,
16
+ } from "./prompts.ts";
17
+ import { AuditorState, CandidateFindingSchema, FileRankingSchema, JudgeReviewSchema } from "./state.ts";
18
+ import { buildRepoTree } from "./tools/repo-tree/tool.ts";
19
+ import { analyzeSolidityFile } from "./tools/solidity-analyzer/tool.ts";
20
+ import { buildReviewBlocks, matchLines, walkDirectory } from "./utils.ts";
21
+
22
+ const llmHaiku = createLLM("anthropic", { model: "claude-haiku-4-5", maxTokens: 20000 });
23
+ const llmOpus = createLLM("anthropic", { model: "claude-opus-4-8", temperature: null, maxTokens: 20000 });
24
+ const llmSonnet = createLLM("anthropic", { model: "claude-sonnet-4-6", maxTokens: 20000 });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  const defineScope: GraphNode<typeof AuditorState> = async (state) => {
27
  logger.info(`defineScope: walking repo at ${state.repoPath}`);
 
38
  logger.debug(`defineScope: doc files: ${JSON.stringify(docFiles)}`);
39
  logger.debug(`defineScope: file tree:\n${fileTree}`);
40
 
41
+ logger.info("defineScope: ranking files by importance");
42
+
43
+ const RankFilesSchema = z.object({ rankings: z.array(FileRankingSchema) });
44
+ const rankingModel = llmHaiku.withStructuredOutput(RankFilesSchema);
45
+
46
+ const { rankings } = await rankingModel.invoke([
47
+ new SystemMessage({ content: [{ type: "text", text: RANK_FILES_PROMPT, cache_control: { type: "ephemeral" } }] }),
48
+ new HumanMessage(
49
+ `Árvore de arquivos:\n\`\`\`\n${fileTree}\n\`\`\`\n\nArquivos Solidity para classificar:\n${solFiles.map((f) => `- ${f}`).join("\n")}`,
50
+ ),
51
+ ]);
52
+
53
+ const sorted = [...rankings].sort((a, b) => b.importance - a.importance);
54
+ logger.info(
55
+ `defineScope: rankings:\n${sorted.map((r) => ` [${r.importance}/5] ${r.filePath} — ${r.reasoning}`).join("\n")}`,
56
+ );
57
+
58
+ const importantFiles = sorted.filter((r) => r.importance >= MIN_FILE_IMPORTANCE).map((r) => r.filePath);
59
+ const skipped = solFiles.length - importantFiles.length;
60
+ if (skipped > 0) {
61
+ logger.info(`defineScope: skipping ${skipped} low-importance file(s) (importance < ${MIN_FILE_IMPORTANCE})`);
62
+ }
63
+
64
+ return { scope: importantFiles, docs: docFiles, fileTree, fileRankings: sorted };
65
  };
66
 
67
  const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
 
75
  }
76
  };
77
 
 
78
  const solidityEntries: { filePath: string; source: string; analysis: string }[] = [];
79
  for (const filePath of state.scope) {
80
  const source = readFile(filePath).slice(0, MAX_SOL_CHARS);
81
  if (!source) continue;
82
+
83
+ const ranking = state.fileRankings.find((r) => r.filePath === filePath);
84
+ const mode = ranking && ranking.importance >= 4 ? "full" : "short";
85
+ const analysis = await analyzeSolidityFile(source, mode, filePath, ranking?.importance);
86
  solidityEntries.push({ filePath, source, analysis });
87
  }
88
 
 
89
  const docEntries: { filePath: string; content: string }[] = [];
90
  for (const filePath of state.docs) {
91
  const content = readFile(filePath).slice(0, MAX_DOC_CHARS);
92
  if (content) docEntries.push({ filePath, content });
93
  }
94
 
 
95
  const parts: string[] = [];
96
 
97
  if (docEntries.length > 0) {
98
+ parts.push("## Documentação\n");
99
  for (const { filePath, content } of docEntries) {
100
  parts.push(`### ${filePath}\n${content}`);
101
  }
102
  }
103
 
104
+ parts.push(`## Árvore de Arquivos\n\n\`\`\`\n${state.fileTree}\n\`\`\``);
 
 
 
105
 
106
+ parts.push("## Análise Estrutural\n");
107
+ for (const { analysis } of solidityEntries) {
108
+ parts.push(analysis);
109
  }
110
 
111
+ const model = llmHaiku.withStructuredOutput(z.object({ context: z.string() }));
112
+ const result = await model.invoke([
113
+ new SystemMessage({
114
+ content: [{ type: "text", text: GATHER_CONTEXT_PROMPT, cache_control: { type: "ephemeral" } }],
115
+ }),
116
+ new HumanMessage(parts.join("\n\n")),
117
+ ]);
118
+
119
+ const fileTreeBlock = `## Árvore de Arquivos\n\n\`\`\`\n${state.fileTree}\n\`\`\``;
120
+ const structuralBlock = `## Análise Estrutural dos Contratos\n\n${solidityEntries.map(({ analysis }) => analysis).join("\n\n---\n\n")}`;
121
+ const repoContext = [result.context, fileTreeBlock, structuralBlock].join("\n\n");
122
 
 
123
  logger.debug(`gatherContext: full context:\n${parts.join("\n\n")}`);
124
+ logger.info(`gatherContext: context built (${repoContext.length} chars)`);
125
+ logger.debug(`gatherContext: compact context:\n${repoContext}`);
126
 
127
+ return { repoContext };
128
  };
129
 
130
  const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
131
+ const model = llmOpus.withStructuredOutput(z.object({ findings: z.array(CandidateFindingSchema) }));
132
+
133
+ const isReflection = state.judgeReviews.length > 0;
 
 
 
 
 
 
 
 
134
 
135
  logger.info(
136
  `findVulnerabilities: invoking LLM for ${state.scope.length} file(s) in parallel (iteration ${state.reflectionCount + 1})`,
137
  );
138
 
139
+ const cachedContext = {
140
+ type: "text" as const,
141
+ text: `Contexto do Protocolo:\n${state.repoContext}`,
142
+ cache_control: { type: "ephemeral" as const },
143
+ };
144
+
145
+ const processFile = async (filePath: string) => {
146
+ let source: string;
147
+ try {
148
+ source = fs.readFileSync(filePath, "utf-8").slice(0, MAX_SOL_CHARS);
149
+ } catch {
150
+ return [];
151
+ }
152
+ if (!source) return [];
153
+
154
+ const fileEntries = isReflection
155
+ ? state.candidateFindings
156
+ .map((f, i) => ({ finding: f, review: state.judgeReviews[i] }))
157
+ .filter(({ finding }) => finding.path === filePath)
158
+ : [];
159
+
160
+ const isRefinement = fileEntries.length > 0;
161
+ const promptText = isRefinement ? REFINE_VULNERABILITIES_PROMPT : FIND_VULNERABILITIES_PROMPT;
162
+ const contractText = isRefinement
163
+ ? `Contrato (${filePath}):\n\n${source}\n\n${buildReviewBlocks(fileEntries, state.reflectionCount)}`
164
+ : `Contrato (${filePath}):\n\n${source}`;
165
+
166
+ logger.debug(`findVulnerabilities: processing ${filePath}`);
167
+
168
+ const result = await model.invoke([
169
+ new SystemMessage({ content: [{ type: "text", text: promptText, cache_control: { type: "ephemeral" } }] }),
170
+ new HumanMessage({ content: [cachedContext, { type: "text", text: contractText }] }),
171
+ ]);
172
+
173
+ return result.findings.map((finding: any) => ({
174
+ ...finding,
175
+ path: filePath,
176
+ location: matchLines(source, finding.codeSnippet) ?? "",
177
+ }));
178
+ };
179
+
180
+ const [firstFile, ...restFiles] = state.scope;
181
+ const firstFindings = firstFile ? await processFile(firstFile) : [];
182
+ const restFindings = await Promise.all(restFiles.map(processFile));
183
+ const candidateFindings = [firstFindings, ...restFindings].flat();
184
 
 
185
  logger.info(`findVulnerabilities: LLM returned ${candidateFindings.length} total candidate finding(s)`);
186
  logger.debug(`findVulnerabilities: findings:\n${JSON.stringify(candidateFindings, null, 2)}`);
187
 
 
198
  };
199
  }
200
 
201
+ const model = llmSonnet.withStructuredOutput(JudgeReviewSchema);
202
 
203
  logger.info(`judgeFindings: reviewing ${state.candidateFindings.length} candidate finding(s) in parallel`);
204
 
205
+ const cachedContext = {
206
+ type: "text" as const,
207
+ text: `Contexto do Protocolo:\n${state.repoContext}`,
208
+ cache_control: { type: "ephemeral" as const },
209
+ };
210
+
211
+ const reviewFinding = async (finding: (typeof state.candidateFindings)[number], i: number) => {
212
+ let source: string;
213
+ try {
214
+ source = fs.readFileSync(finding.path, "utf-8").slice(0, MAX_SOL_CHARS);
215
+ } catch {
216
+ source = "";
217
+ }
218
+
219
+ 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\`\`\``;
220
+
221
+ logger.debug(`judgeFindings: reviewing finding ${i + 1}: ${finding.title}`);
222
+ return model.invoke([
223
+ new SystemMessage({
224
+ content: [{ type: "text", text: JUDGE_FINDINGS_PROMPT, cache_control: { type: "ephemeral" } }],
225
+ }),
226
+ new HumanMessage({
227
+ content: [
228
+ cachedContext,
229
+ { type: "text", text: `Contrato (${finding.path}):\n\n${source}\n\nAchado para Revisão:\n\n${findingText}` },
230
+ ],
231
+ }),
232
+ ]);
233
+ };
234
+
235
+ const [firstFinding, ...restFindings] = state.candidateFindings;
236
+ const firstReview = await reviewFinding(firstFinding, 0);
237
+ const restReviews = await Promise.all(restFindings.map((f, i) => reviewFinding(f, i + 1)));
238
+ const reviews = [firstReview, ...restReviews];
239
 
240
  const confirmedEntries = state.candidateFindings
241
  .map((finding, i) => ({ finding, review: reviews[i] }))
src/agents/auditor/config.ts CHANGED
@@ -20,4 +20,5 @@ export const SKIP_DIRS = new Set([
20
  export const MAX_DEPTH = 6;
21
  export const MAX_DOC_CHARS = 12_000;
22
  export const MAX_SOL_CHARS = 40_000;
23
- export const MAX_REFLECTIONS = 1;
 
 
20
  export const MAX_DEPTH = 6;
21
  export const MAX_DOC_CHARS = 12_000;
22
  export const MAX_SOL_CHARS = 40_000;
23
+ export const MAX_REFLECTIONS = 3;
24
+ export const MIN_FILE_IMPORTANCE = 3;
src/agents/auditor/prompts.ts CHANGED
@@ -1,50 +1,131 @@
1
- export const GATHER_CONTEXT_PROMPT = `Você é um especialista em segurança de smart contracts. Você receberá documentação, uma análise estrutural e o código-fonte completo de todos os contratos Solidity em escopo. Produza um contexto detalhado do protocolo que guiará a descoberta de vulnerabilidades.
2
 
3
- Estruture sua resposta nas seguintes seções:
 
 
4
 
5
- ## 1. Visão Geral dos Contratos
6
- Para cada contrato: seu propósito, tipo (contract/interface/library/abstract), cadeia de herança e principais dependências de outros contratos em escopo ou protocolos externos.
 
 
 
 
7
 
8
- ## 2. Mapa de Estado e Armazenamento
9
- Liste todas as variáveis de estado relevantes entre os contratos, o que representam e quais funções as leem ou escrevem. Sinalize armazenamento compartilhado ou herdado.
10
 
11
- ## 3. Fluxos Principais
12
- Trace os principais caminhos de execução e transições de estado de ponta a ponta entre contratos (ex.: depósito → cunhar shares → atualizar recompensas; saque → queimar shares → transferir ETH). Inclua chamadas entre contratos.
13
 
14
- ## 4. Invariantes
15
- Condições que devem sempre ser verdadeiras (ex.: "o supply total deve ser igual à soma de todos os saldos", "o saldo de ETH do contrato ≥ soma de todos os depósitos dos usuários"). Derive-as tanto do código-fonte quanto da documentação.
16
 
17
- ## 5. Premissas de Design
18
- O que o protocolo assume sobre chamadores, contratos externos, oráculos, chaves de administrador e comportamento de tokens (ex.: "tokens são compatíveis com ERC-20", "o admin é confiável", "sem tokens com taxa de transferência").
19
 
20
- ## 6. Regras de Negócio
21
- Controles de acesso, estruturas de taxas, timelocks, limites, mecanismos de pausa, padrões de atualização e quaisquer outras restrições de domínio.
22
 
23
- Seja preciso e exaustivo — quanto mais rico o contexto, com mais precisão as vulnerabilidades podem ser identificadas e validadas.`;
 
24
 
25
- export const FIND_VULNERABILITIES_PROMPT = `Você é um auditor especialista em segurança de smart contracts com foco em Solidity. Analise sistematicamente o código-fonte do contrato e o contexto do protocolo para identificar vulnerabilidades de segurança.
 
 
 
26
 
27
- Para cada vulnerabilidade, forneça TODOS os seguintes campos:
 
 
 
28
 
29
- - **title**: Nome curto e preciso (ex.: "Reentrância em withdraw", "Controle de acesso ausente em setFee").
30
- - **description**: Explique o comportamento ESPERADO versus o comportamento OBSERVADO (vulnerável) em 2 a 4 frases.
31
- - **recommendation**: Correção específica e acionável (ex.: "Aplicar o padrão checks-effects-interactions", "Adicionar o modificador onlyOwner").
32
- - **severity**: Um de "high" (perda direta de fundos ou tomada de controle do contrato), "medium" (risco indireto ou condicional), "low" (problema de boas práticas, sem risco financeiro imediato).
33
- - **codeSnippet**: O bloco de código vulnerável exatamente como aparece no código-fonte.
34
 
35
- Categorias de vulnerabilidades a verificar sistematicamente: reentrância (simples e entre funções), controle de acesso, overflow/underflow de inteiros, manipulação de oráculo, ataques de flash loan, front-running/MEV, replay de assinatura, colisões de armazenamento, proxies não inicializados, delegatecall inseguro, griefing de gas, negação de serviço, perda de precisão e violações de lógica/regras de negócio.
 
 
36
 
37
- Se feedback do juiz de uma iteração anterior for fornecido, remova os falsos positivos confirmados da sua lista e refine ou expanda os achados restantes com base na crítica.`;
 
38
 
39
- export const JUDGE_FINDINGS_PROMPT = `Você é um revisor rigoroso de segurança de smart contracts. Avalie cada vulnerabilidade candidata submetida pelo auditor e determine se é um verdadeiro positivo ou um falso positivo.
40
 
41
- Para cada achado, forneça TODOS os seguintes campos:
42
 
43
- - **review**: Análise detalhada (3 a 6 frases) explicando por que a vulnerabilidade é ou não real. Referencie código específico, invariantes do protocolo, pré-condições e controles mitigadores.
44
- - **isFalsePositive**: true se o achado NÃO for explorável na prática; false se for uma vulnerabilidade real.
45
- - **confidence**: Número inteiro de 0 a 100 refletindo sua confiança no veredicto.
46
- - **exploitablePaths**: Array de strings. Se for verdadeiro positivo, forneça caminhos concretos confirmando a explorabilidade com valores reais. Cada rastreamento deve descrever os passos do atacante com entradas/valores realistas (ex.: "1. Atacante chama deposit(100 ETH) 2. Contrato do atacante no fallback chama withdraw() novamente antes da atualização do saldo 3. Atacante drena 100 ETH duas vezes"). Se for falso positivo, forneça o raciocínio que bloqueia o exploit.
47
 
48
- Um achado é falso positivo somente se: o caminho de exploit for inacessível dados os controles de acesso ou pré-condições, já estiver totalmente mitigado pelo código, exigir condições impossíveis ou economicamente inviáveis, ou for explicitamente documentado como comportamento esperado nas premissas do protocolo.
49
 
50
- Você deve fornecer exatamente um objeto de revisão por achado, na mesma ordem em que os achados foram apresentados.`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const RANK_FILES_PROMPT = `Você é um especialista em segurança de smart contracts. Dada a árvore de arquivos do repositório e uma lista de contratos Solidity, classifique cada arquivo pela sua importância para a descoberta de vulnerabilidades de segurança.
2
 
3
+ Para cada arquivo atribua:
4
+ - importance: inteiro de 1 (menos importante) a 5 (mais importante)
5
+ - reasoning: uma frase concisa justificando a classificação (mencione o papel específico do contrato, não categorias genéricas)
6
 
7
+ Critérios de classificação:
8
+ - Importância 5: lógica central do protocolo (vaults, pools, engines), custódia de tokens/ETH, mecanismos de upgrade/proxy, cálculos financeiros críticos (preço, juros, liquidação), controle de acesso raiz.
9
+ - Importância 4: contratos que movem valor e interagem diretamente com os de importância 5, máquinas de estado complexas, distribuição de taxas/recompensas, roteadores de entrada.
10
+ - Importância 3: helpers periféricos com lógica de negócio, bibliotecas reutilizáveis com efeitos colaterais, governança com timelocks.
11
+ - Importância 2: interfaces, adaptadores, wrappers triviais, contratos utilitários sem lógica crítica.
12
+ - Importância 1: views somente-leitura, arquivos de constantes/configuração pura, mocks e scripts de deploy.
13
 
14
+ Retorne a classificação de TODOS os arquivos fornecidos, sem omitir nenhum.`;
 
15
 
16
+ export const GATHER_CONTEXT_PROMPT = `Você é um especialista em segurança de smart contracts criando um modelo mental preciso do protocolo para auditoria. Você receberá documentação e a análise estrutural dos contratos Solidity em escopo.
 
17
 
18
+ **REGRA DE FIDELIDADE FACTUAL — NÃO NEGOCIÁVEL**: Baseie-se EXCLUSIVAMENTE no que está EXPLICITAMENTE presente no código-fonte (linhas de código, NatSpec, comentários inline) ou na documentação fornecida. NUNCA infira, suponha, extrapole ou "complete" informações que não estejam literalmente escritas nas fontes. Se uma informação não aparece explicitamente, NÃO a inclua e NÃO a invente. Esta regra se aplica a TODAS as seções. **Na seção Trust Assumptions esta proibição é ABSOLUTA: se não há declaração explícita (código, NatSpec, comentário ou doc) sobre uma suposição, ela não existe para você.**
 
19
 
20
+ Produza um contexto factual e detalhado de auditoria sem introduções, sem padding e sem repetições. Preserve nomes concretos (funções, variáveis, tipos, valores numéricos) exatamente como aparecem no código. Sem prosa explicativa.
 
21
 
22
+ A árvore de arquivos do repositório e a análise estrutural completa de cada contrato serão anexadas automaticamente ao final do contexto — **não as duplique**. Concentre-se nas seções de síntese abaixo.
 
23
 
24
+ ## Visão geral
25
+ Descreva o propósito do protocolo, o fluxo econômico principal, os participantes envolvidos e os ativos protegidos — apenas o que estiver explicitamente declarado no código ou na documentação.
26
 
27
+ ## Estado Crítico
28
+ Todas as variáveis de estado com impacto em lógica de negócio, segurança ou contabilidade, extraídas diretamente da seção Storage da análise estrutural.
29
+ Formato por bullet: \`filePath::nomeVar (tipo, visibilidade) — NatSpec/comentário se presente — funções que escrevem nela — impacto se manipulada\`.
30
+ Omita apenas constantes e imutáveis puramente administrativas (nome do token, símbolo, decimals, versão de string).
31
 
32
+ ## Fluxos Principais (máx. 5 fluxos, 3–6 passos cada)
33
+ Apenas os caminhos críticos ponta a ponta que movem valor ou alteram estado relevante, derivados das funções e call graphs observados no código.
34
+ Formato por passo: \`ação (função) → efeito colateral → variável/estado alterado\`.
35
+ Inclua chamadas cross-contract quando materiais para entender superfície de ataque.
36
 
37
+ ## Invariantes e Propriedades
38
+ Condições que devem ser verdadeiras para o protocolo operar corretamente, derivadas APENAS de \`require\`/\`assert\`/\`revert\` explícitos no código, NatSpec \`@dev\`, ou comentários que as declarem literalmente. Separe em dois grupos:
39
+ - **Econômicas**: balanços, totais, proporções (ex.: \`totalDebt == Σ userDebt[i]\`, \`reservas >= totalSupply * exchangeRate\`)
40
+ - **De controle**: acesso, sequência de operações, transições de estado permitidas
 
41
 
42
+ ## Trust Assumptions
43
+ **SOMENTE o que estiver EXPLICITAMENTE declarado** em código-fonte (require, NatSpec, comentários inline) ou na documentação. **NÃO inferir. NÃO supor. NÃO extrapolar.** Se não há declaração explícita sobre confiança em um componente externo ou comportamento esperado, ele NÃO entra nesta seção — mesmo que pareça óbvio.
44
+ Bullets curtos com referência à fonte (ex.: "owner pode pausar o contrato — \`onlyOwner\` em \`pause()\`").
45
 
46
+ ## Regras de Negócio e Restrições de Segurança
47
+ Em bullets: roles e modifiers relevantes (nomes exatos do código), limites numéricos (apenas valores literais presentes no código-fonte), taxas e destinatários, timelocks, pausabilidade, condições de upgrade, restrições de whitelist/blacklist. Inclua apenas regras com impacto direto em vetores de ataque.
48
 
49
+ **Formato obrigatório**: bullets e frases curtas. Dados concretos (nomes de funções, variáveis, valores numéricos) exatamente como aparecem no código. Sem prosa explicativa.`;
50
 
51
+ export const FIND_VULNERABILITIES_PROMPT = `Você é um auditor especialista em segurança de smart contracts com profundo conhecimento em Solidity, execução EVM e design de protocolos. Assuma que todos os usuários são adversariais e estão ativamente tentando explorar o contrato.
52
 
53
+ Sua tarefa é analisar o código-fonte fornecido linha a linha e identificar todas as vulnerabilidades de segurança, design, lógica e econômicas. Qualquer discrepância entre a implementação e o comportamento esperado, as suposições do protocolo ou o design econômico deve ser reportada como vulnerabilidade, mesmo que o contrato execute sem erros em runtime.
 
 
 
54
 
55
+ **Escopo obrigatório da análise**
56
 
57
+ Além das categorias técnicas listadas abaixo, sua análise deve cobrir:
58
+
59
+ - **Invariantes de protocolo**: identifique invariantes implícitas e explícitas (ex.: depósitos devem igualar saques, ativos devem permanecer colateralizados, recompensas devem corresponder aos inputs) e verifique se podem ser quebradas.
60
+ - **Fluxos de valor**: analise exaustivamente todas as transferências de valor (taxas, saldos, depósitos, saques, recompensas, deltas), verificando: quem provê os fundos, quem os recebe, e se os fundos são corretamente custodiados (escrowed) antes da transferência.
61
+ - **Validação de ownership**: verifique se usuários podem interagir com tokens, NFTs ou permissões que não controlam.
62
+ - **Observabilidade**: avalie se eventos, logs e chamadas de métodos representam corretamente as ações do protocolo. Emissão ausente, incorreta ou enganosa é uma vulnerabilidade.
63
+ - **Implementação vs. intenção**: qualquer desvio entre o comportamento implementado e o design pretendido do protocolo deve ser reportado.
64
+
65
+ ## Categorias técnicas a verificar sistematicamente
66
+
67
+ Reentrância (simples, cross-function, cross-contract, read-only), controle de acesso (funções privilegiadas desprotegidas, erros em herança de roles), overflow/underflow (Solidity <0.8 ou uso de \`unchecked\`), manipulação de oráculo (TWAP curto, preço spot, valor de reserves), ataques de flash loan (price impact, liquidações artificiais), front-running e MEV (sandwich, race condition em aprovações), replay de assinatura (nonce ausente, falta de chainId), colisões de storage (proxies, delegatecall), proxies não inicializados (initializer sem proteção), delegatecall inseguro (destino controlável pelo usuário), griefing de gas (loops ilimitados, arrays crescentes), negação de serviço (push payments, dependência de chamada externa), perda de precisão (divisão antes de multiplicação, truncamento acumulativo), lógica de negócio (violação de invariantes, casos de borda em math financeira, race conditions de estado).
68
+
69
+ ## Formato de saída
70
+
71
+ Para cada vulnerabilidade encontrada, forneça OBRIGATORIAMENTE todos os campos abaixo. Cada entrada deve ser **atômica**: reporte exatamente um problema por entrada. Não agrupe múltiplos problemas em um único achado, mesmo que ocorram na mesma função ou linha. Não há limite para o número de vulnerabilidades reportadas.
72
+
73
+ - **title**: Nome curto e preciso (ex.: "Reentrância em \`withdraw\`", "Controle de acesso ausente em \`setFee\`").
74
+ - **description**: Descreva (a) o comportamento **esperado** pelo protocolo, (b) o comportamento **observado** no código vulnerável, e (c) o impacto concreto se explorado. Mínimo 3 frases, máximo 5.
75
+ - **exploit_scenario**: Descreva um cenário concreto e passo a passo de como um atacante exploraria a vulnerabilidade.
76
+ - **recommendation**: Correção específica e acionável com referência ao padrão ou mecanismo correto (ex.: "Aplicar checks-effects-interactions: mover \`balances[msg.sender] -= amount\` para antes da chamada externa").
77
+ - **severity**: Exatamente um de: \`"high"\` (perda direta de fundos ou tomada de controle do contrato), \`"medium"\` (risco indireto ou condicional), \`"low"\` (problema de boas práticas, sem risco financeiro imediato).
78
+ - **location**: Função e/ou número de linha onde o problema ocorre.
79
+ - **codeSnippet**: O trecho exato e completo do código vulnerável, copiado literalmente do código-fonte. **Proibido** usar reticências (\`...\`), omissões, pseudocódigo ou paráfrases. Inclua as linhas exatas conforme aparecem no arquivo, com indentação original preservada. Se o snippet for maior que 40 linhas, inclua o intervalo completo sem cortes.
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
+
112
+ Para cada achado, forneça OBRIGATORIAMENTE todos os campos abaixo:
113
+
114
+ - **review**: Análise técnica detalhada (3 a 6 frases) explicando o veredicto. Referencie: (a) o código específico envolvido, (b) invariantes ou premissas do protocolo que confirmam ou bloqueiam o exploit, (c) pré-condições necessárias para exploração, (d) controles mitigadores existentes que o auditor pode ter ignorado. Seja preciso — cite nomes de funções, variáveis e valores.
115
+ - **isFalsePositive**: \`true\` se o achado NÃO for explorável na prática; \`false\` se for uma vulnerabilidade real.
116
+ - **confidence**: Inteiro de 0 a 100 refletindo sua certeza no veredicto. Use < 60 apenas quando existir ambiguidade genuína no código.
117
+ - **exploitablePaths**: Retorne uma array de strings.
118
+ - Se verdadeiro positivo (\`isFalsePositive: false\`): forneça 1 a 3 caminhos concretos de exploit, cada um com passos numerados, entradas realistas e estado do contrato antes/depois. Ex.: ["1. Atacante chama flashLoan(500k USDC). 2. No callback, chama deposit() inflando reserves. 3. Chama withdraw() com preço manipulado. 4. Lucra 50k USDC. Estado: reserves inflado temporariamente, totalShares inalterado."].
119
+ - Se falso positivo (\`isFalsePositive: true\`): forneça o raciocínio exato que bloqueia cada caminho de exploit tentado pelo auditor.
120
+
121
+ ## Critérios para falso positivo (aplique com rigor — não seja permissivo)
122
+
123
+ 1. O caminho de exploit é bloqueado por controle de acesso verificável no código.
124
+ 2. A vulnerabilidade já é totalmente mitigada por outro mecanismo no código (ex.: nonReentrant, onlyOwner, require com validação suficiente).
125
+ 3. A condição necessária para o exploit é impossível ou economicamente inviável dado o modelo do protocolo (ex.: requer ser o próprio contrato, ou lucro < custo de gas em qualquer cenário realista).
126
+ 4. O comportamento é explicitamente documentado como intencional nas premissas de design do protocolo.
127
+
128
+ ## Critérios para verdadeiro positivo
129
+ - Existe pelo menos um caminho de exploit concreto e realista que viola uma invariante ou permite extração de valor não autorizada.
130
+ - Não exige condições impossíveis nem assume acesso privilegiado não disponível ao atacante.
131
+ - Inclui qualquer discrepância entre a implementação e a intenção, premissas ou objetivos documentados — mesmo que o contrato opere sem erros em runtime. Isso abrange problemas de segurança, inconsistências de design, desalinhamentos econômicos e falhas de observabilidade, independentemente de exploitabilidade direta.`;
src/agents/auditor/state.ts CHANGED
@@ -1,6 +1,12 @@
1
  import { StateSchema } from "@langchain/langgraph";
2
  import { z } from "zod";
3
 
 
 
 
 
 
 
4
  export const CandidateFindingSchema = z.object({
5
  title: z.string(),
6
  description: z.string(),
@@ -34,6 +40,7 @@ export const AuditorState = new StateSchema({
34
  scope: z.array(z.string()).default([]),
35
  docs: z.array(z.string()).default([]),
36
  fileTree: z.string().default(""),
 
37
  repoContext: z.string().default(""),
38
  candidateFindings: z.array(LocatedFindingSchema).default([]),
39
  judgeReviews: z.array(JudgeReviewSchema).default([]),
 
1
  import { StateSchema } from "@langchain/langgraph";
2
  import { z } from "zod";
3
 
4
+ export const FileRankingSchema = z.object({
5
+ filePath: z.string(),
6
+ importance: z.number().int().min(1).max(5),
7
+ reasoning: z.string(),
8
+ });
9
+
10
  export const CandidateFindingSchema = z.object({
11
  title: z.string(),
12
  description: z.string(),
 
40
  scope: z.array(z.string()).default([]),
41
  docs: z.array(z.string()).default([]),
42
  fileTree: z.string().default(""),
43
+ fileRankings: z.array(FileRankingSchema).default([]),
44
  repoContext: z.string().default(""),
45
  candidateFindings: z.array(LocatedFindingSchema).default([]),
46
  judgeReviews: z.array(JudgeReviewSchema).default([]),
src/agents/auditor/tools/{repo-tree-tool.ts → repo-tree/tool.ts} RENAMED
@@ -4,7 +4,7 @@ import path from "node:path";
4
  import { tool } from "langchain";
5
  import { z } from "zod";
6
 
7
- import { DOC_BASENAMES, DOC_EXTS, MAX_DEPTH, SKIP_DIRS, SOL_EXT, SOL_TEST_SUFFIXES } from "../config.ts";
8
 
9
  const CONFIG_FILES = new Set([
10
  "foundry.toml",
@@ -90,14 +90,11 @@ export const buildRepoTree = (repoPath: string): string => {
90
  return `${repoName}/\n${renderTree(nodes, "")}`;
91
  };
92
 
93
- export const repoTreeTool = tool(
94
- async ({ repoPath }) => buildRepoTree(repoPath),
95
- {
96
- name: "repo_tree",
97
- description:
98
- "Walk a repository and return a file-system tree of relevant files tagged by kind: [sol] for auditable Solidity contracts, [test] for Solidity test files, [doc] for documentation, and [config] for project config files. Use this during Define Scope to understand repository layout before selecting which files to audit.",
99
- schema: z.object({
100
- repoPath: z.string().describe("Absolute path to the repository root."),
101
- }),
102
- },
103
- );
 
4
  import { tool } from "langchain";
5
  import { z } from "zod";
6
 
7
+ import { DOC_BASENAMES, DOC_EXTS, MAX_DEPTH, SKIP_DIRS, SOL_EXT, SOL_TEST_SUFFIXES } from "../../config.ts";
8
 
9
  const CONFIG_FILES = new Set([
10
  "foundry.toml",
 
90
  return `${repoName}/\n${renderTree(nodes, "")}`;
91
  };
92
 
93
+ export const repoTreeTool = tool(async ({ repoPath }) => buildRepoTree(repoPath), {
94
+ name: "repo_tree",
95
+ description:
96
+ "Walk a repository and return a file-system tree of relevant files tagged by kind: [sol] for auditable Solidity contracts, [test] for Solidity test files, [doc] for documentation, and [config] for project config files. Use this during Define Scope to understand repository layout before selecting which files to audit.",
97
+ schema: z.object({
98
+ repoPath: z.string().describe("Absolute path to the repository root."),
99
+ }),
100
+ });
 
 
 
src/agents/auditor/tools/solidity-analyzer-tool.ts DELETED
@@ -1,567 +0,0 @@
1
- import { parse, visit } from "@solidity-parser/parser";
2
- import { tool } from "langchain";
3
- import { z } from "zod";
4
-
5
- const ASSIGNMENT_OPS = new Set(["=", "+=", "-=", "*=", "/=", "%=", "|=", "&=", "^=", "<<=", ">>=", "**="]);
6
- const BUILTIN_NAMESPACES = new Set(["abi", "block", "msg", "tx", "bytes", "string", "type"]);
7
-
8
- interface StateVar {
9
- name: string;
10
- type: string;
11
- visibility: string;
12
- constant: boolean;
13
- immutable: boolean;
14
- }
15
-
16
- interface EventDef {
17
- name: string;
18
- params: string[];
19
- anonymous: boolean;
20
- }
21
-
22
- interface ModifierDef {
23
- name: string;
24
- params: string[];
25
- }
26
-
27
- interface FunctionDef {
28
- name: string;
29
- isConstructor: boolean;
30
- isReceive: boolean;
31
- isFallback: boolean;
32
- visibility: string;
33
- mutability: string;
34
- params: string[];
35
- returns: string[];
36
- modifiers: string[];
37
- internalCalls: string[];
38
- externalCalls: string[];
39
- stateReads: string[];
40
- stateWrites: string[];
41
- }
42
-
43
- interface ContractAnalysis {
44
- name: string;
45
- kind: string;
46
- baseContracts: string[];
47
- usingFor: string[];
48
- stateVars: StateVar[];
49
- events: EventDef[];
50
- modifiers: ModifierDef[];
51
- functions: FunctionDef[];
52
- }
53
-
54
- const typeToString = (node: any): string => {
55
- if (!node) return "unknown";
56
-
57
- switch (node.type) {
58
- case "ElementaryTypeName":
59
- return node.name as string;
60
- case "UserDefinedTypeName":
61
- return (node.namePath ?? node.name) as string;
62
- case "ArrayTypeName":
63
- return `${typeToString(node.baseTypeName)}[${node.length ?? ""}]`;
64
- case "Mapping":
65
- return `mapping(${typeToString(node.keyType)} => ${typeToString(node.valueType)})`;
66
- case "FunctionTypeName":
67
- return "function";
68
- default:
69
- return "unknown";
70
- }
71
- };
72
-
73
- const paramToString = (p: any) => {
74
- if (!p) return "?";
75
- const type = typeToString(p.typeName);
76
- return p.name ? `${type} ${p.name}` : type;
77
- };
78
-
79
- const collectLHSRoots = (node: any, targets: Set<string>) => {
80
- if (!node) return;
81
- switch (node.type) {
82
- case "Identifier":
83
- targets.add(node.name as string);
84
- break;
85
- case "MemberAccess":
86
- collectLHSRoots(node.expression, targets);
87
- break;
88
- case "IndexAccess":
89
- collectLHSRoots(node.base, targets);
90
- break;
91
- case "TupleExpression":
92
- for (const c of node.components ?? []) collectLHSRoots(c, targets);
93
- break;
94
- }
95
- };
96
-
97
- const analyzeFunction = (funcNode: any, stateVarNames: Set<string>) => {
98
- const internalCalls = new Set<string>();
99
- const externalCalls = new Set<string>();
100
- const writeTargets = new Set<string>();
101
- const allStateAccesses = new Set<string>();
102
- const localVars = new Set<string>();
103
-
104
- if (!funcNode.body) {
105
- return { internalCalls: [], externalCalls: [], stateReads: [], stateWrites: [] };
106
- }
107
-
108
- // Collect function params and return params as locals so they don't shadow state vars
109
- for (const p of funcNode.parameters ?? []) {
110
- if (p?.name) localVars.add(p.name as string);
111
- }
112
- for (const p of funcNode.returnParameters ?? []) {
113
- if (p?.name) localVars.add(p.name as string);
114
- }
115
-
116
- // Collect local variable declarations
117
- visit(funcNode.body, {
118
- VariableDeclarationStatement: (node: any) => {
119
- for (const v of node.variables ?? []) {
120
- if (v?.name) localVars.add(v.name as string);
121
- }
122
- },
123
- });
124
-
125
- const effectiveStateVars = new Set([...stateVarNames].filter((v) => !localVars.has(v)));
126
-
127
- // Collect write targets from assignment LHS, unary mutations, and delete
128
- visit(funcNode.body, {
129
- ExpressionStatement: (node: any) => {
130
- const expr = node.expression;
131
- if (expr?.type === "BinaryOperation" && ASSIGNMENT_OPS.has(expr.operator as string)) {
132
- collectLHSRoots(expr.left, writeTargets);
133
- }
134
- // Handle ++, --, and delete — all work on any lvalue (arr[i]++, delete s.field, etc.)
135
- if (
136
- expr?.type === "UnaryOperation" &&
137
- (expr.operator === "++" || expr.operator === "--" || expr.operator === "delete")
138
- ) {
139
- collectLHSRoots(expr.subExpression, writeTargets);
140
- }
141
- },
142
- });
143
-
144
- // Collect calls and state-var identifier accesses
145
- visit(funcNode.body, {
146
- FunctionCall: (node: any) => {
147
- const expr = node.expression;
148
- if (expr?.type === "Identifier") {
149
- internalCalls.add(expr.name as string);
150
- } else if (expr?.type === "MemberAccess") {
151
- const base = expr.expression;
152
- if (base?.type === "Identifier" && (base.name === "this" || base.name === "super")) {
153
- internalCalls.add(expr.memberName as string);
154
- } else if (base?.type === "Identifier" && BUILTIN_NAMESPACES.has(base.name as string)) {
155
- // abi.encode, block.xxx, msg.xxx, etc. — not external calls
156
- } else {
157
- const baseStr = base?.type === "Identifier" ? (base.name as string) : "<expr>";
158
- externalCalls.add(`${baseStr}.${expr.memberName as string}`);
159
- }
160
- }
161
- },
162
- Identifier: (node: any) => {
163
- if (effectiveStateVars.has(node.name as string)) {
164
- allStateAccesses.add(node.name as string);
165
- }
166
- },
167
- });
168
-
169
- const stateWrites = [...allStateAccesses].filter((v) => writeTargets.has(v));
170
- // A var can be in both — e.g. x = x + 1 is both a read and a write.
171
- const stateReads = [...allStateAccesses];
172
-
173
- return {
174
- internalCalls: [...internalCalls],
175
- externalCalls: [...externalCalls],
176
- stateReads,
177
- stateWrites,
178
- };
179
- };
180
-
181
- const hasCycle = (start: string, current: string, callMap: Map<string, string[]>, visited: Set<string>) => {
182
- for (const callee of callMap.get(current) ?? []) {
183
- if (callee === start) return true;
184
- if (!visited.has(callee)) {
185
- visited.add(callee);
186
- if (hasCycle(start, callee, callMap, visited)) return true;
187
- }
188
- }
189
-
190
- return false;
191
- };
192
-
193
- const fnLabel = (fn: FunctionDef) => {
194
- if (fn.isConstructor) return "constructor";
195
- if (fn.isReceive) return "receive";
196
- if (fn.isFallback) return "fallback";
197
- return fn.name;
198
- };
199
-
200
- const generateShortMarkdown = (imports: string[], contracts: ContractAnalysis[]) => {
201
- const lines: string[] = [];
202
- lines.push("# Solidity Analysis\n");
203
-
204
- if (imports.length > 0) {
205
- lines.push(`**Imports:** ${imports.map((i) => `\`${i}\``).join(", ")}\n`);
206
- }
207
-
208
- for (const contract of contracts) {
209
- const inheritance =
210
- contract.baseContracts.length > 0 ? ` : ${contract.baseContracts.map((b) => `\`${b}\``).join(", ")}` : "";
211
- lines.push(`---\n\n## \`${contract.name}\` (${contract.kind})${inheritance}\n`);
212
-
213
- // State variables — one line, name:type
214
- if (contract.stateVars.length > 0) {
215
- const vars = contract.stateVars.map((v) => {
216
- const flags = [v.constant && "constant", v.immutable && "immutable"].filter(Boolean);
217
- const suffix = flags.length > 0 ? `, ${flags.join(", ")}` : "";
218
- return `\`${v.name}: ${v.type}\` (${v.visibility}${suffix})`;
219
- });
220
- lines.push(`**State:** ${vars.join(", ")}\n`);
221
- }
222
-
223
- // Modifiers — names only
224
- if (contract.modifiers.length > 0) {
225
- const mods = contract.modifiers.map(
226
- (m) => `\`${m.name}${m.params.length > 0 ? `(${m.params.join(", ")})` : ""}\``,
227
- );
228
- lines.push(`**Modifiers:** ${mods.join(", ")}\n`);
229
- }
230
-
231
- // Events — name + params, one line each
232
- if (contract.events.length > 0) {
233
- const evts = contract.events.map((e) => `\`${e.name}(${e.params.join(", ")})\``);
234
- lines.push(`**Events:** ${evts.join(", ")}\n`);
235
- }
236
-
237
- // Function list — compact, one line per function
238
- if (contract.functions.length > 0) {
239
- lines.push("**Functions:**");
240
- for (const fn of contract.functions) {
241
- const label = fnLabel(fn);
242
- const params = fn.params.join(", ");
243
- const ret = fn.returns.length > 0 ? ` → ${fn.returns.join(", ")}` : "";
244
- const mods = fn.modifiers.length > 0 ? ` [${fn.modifiers.join(", ")}]` : "";
245
- lines.push(`- \`${label}(${params})${ret}\` — ${fn.visibility} ${fn.mutability}${mods}`);
246
- }
247
- lines.push("");
248
- }
249
-
250
- // External calls — only functions that make them
251
- const externalFuncs = contract.functions.filter((f) => f.externalCalls.length > 0);
252
- if (externalFuncs.length > 0) {
253
- lines.push("**External Calls:**");
254
- for (const fn of externalFuncs) {
255
- lines.push(`- \`${fnLabel(fn)}\`: ${fn.externalCalls.map((c) => `\`${c}\``).join(", ")}`);
256
- }
257
- lines.push("");
258
- }
259
- }
260
-
261
- return lines.join("\n");
262
- };
263
-
264
- const generateMarkdown = (imports: string[], contracts: ContractAnalysis[]) => {
265
- const lines: string[] = [];
266
- lines.push("# Solidity Contract Analysis\n");
267
-
268
- // Imports
269
- lines.push("## Imports\n");
270
- if (imports.length === 0) {
271
- lines.push("_No imports._\n");
272
- } else {
273
- for (const imp of imports) lines.push(`- \`${imp}\``);
274
- lines.push("");
275
- }
276
-
277
- for (const contract of contracts) {
278
- const kindLabel = contract.kind.charAt(0).toUpperCase() + contract.kind.slice(1);
279
- lines.push(`---\n\n## ${kindLabel}: \`${contract.name}\`\n`);
280
-
281
- // Inheritance
282
- lines.push("### Inheritance\n");
283
- if (contract.baseContracts.length === 0) {
284
- lines.push("_None._\n");
285
- } else {
286
- for (const base of contract.baseContracts) lines.push(`- \`${base}\``);
287
- lines.push("");
288
- }
289
-
290
- // Using For
291
- if (contract.usingFor.length > 0) {
292
- lines.push("### Using For\n");
293
- for (const u of contract.usingFor) lines.push(`- ${u}`);
294
- lines.push("");
295
- }
296
-
297
- // Storage layout
298
- lines.push("### Storage Layout (State Variables)\n");
299
- if (contract.stateVars.length === 0) {
300
- lines.push("_No state variables._\n");
301
- } else {
302
- lines.push("| Slot | Name | Type | Visibility | Flags |");
303
- lines.push("|------|------|------|------------|-------|");
304
- contract.stateVars.forEach((v, i) => {
305
- const flags = [v.constant && "constant", v.immutable && "immutable"].filter(Boolean).join(", ");
306
- lines.push(`| ${i} | \`${v.name}\` | \`${v.type}\` | ${v.visibility} | ${flags} |`);
307
- });
308
- lines.push("");
309
- }
310
-
311
- // Events
312
- lines.push("### Events\n");
313
- if (contract.events.length === 0) {
314
- lines.push("_No events._\n");
315
- } else {
316
- for (const evt of contract.events) {
317
- const params = evt.params.join(", ");
318
- lines.push(`- **\`${evt.name}\`**\`(${params})\`${evt.anonymous ? " _(anonymous)_" : ""}`);
319
- }
320
- lines.push("");
321
- }
322
-
323
- // Modifiers
324
- lines.push("### Modifiers\n");
325
- if (contract.modifiers.length === 0) {
326
- lines.push("_No modifiers._\n");
327
- } else {
328
- for (const mod of contract.modifiers) {
329
- lines.push(`- **\`${mod.name}\`**\`(${mod.params.join(", ")})\``);
330
- }
331
- lines.push("");
332
- }
333
-
334
- // Function list
335
- lines.push("### Function List\n");
336
- if (contract.functions.length === 0) {
337
- lines.push("_No functions._\n");
338
- } else {
339
- lines.push("| Name | Visibility | Mutability | Parameters | Returns | Modifiers |");
340
- lines.push("|------|------------|------------|------------|---------|-----------|");
341
- for (const fn of contract.functions) {
342
- lines.push(
343
- `| \`${fnLabel(fn)}\` | ${fn.visibility} | ${fn.mutability} | \`${fn.params.join(", ")}\` | \`${fn.returns.join(", ")}\` | ${fn.modifiers.join(", ")} |`,
344
- );
345
- }
346
- lines.push("");
347
- }
348
-
349
- // Call graph
350
- lines.push("### Call Graph\n");
351
- const hasCalls = contract.functions.some((f) => f.internalCalls.length > 0 || f.externalCalls.length > 0);
352
- if (!hasCalls) {
353
- lines.push("_No function calls detected._\n");
354
- } else {
355
- for (const fn of contract.functions) {
356
- if (fn.internalCalls.length === 0 && fn.externalCalls.length === 0) continue;
357
- lines.push(`**\`${fnLabel(fn)}\`**`);
358
- for (const call of fn.internalCalls) lines.push(` - → \`${call}\` _(internal)_`);
359
- for (const call of fn.externalCalls) lines.push(` - → \`${call}\` _(external)_`);
360
- }
361
- lines.push("");
362
- }
363
-
364
- // External calls
365
- lines.push("### External Calls\n");
366
- const externalFuncs = contract.functions.filter((f) => f.externalCalls.length > 0);
367
- if (externalFuncs.length === 0) {
368
- lines.push("_No external calls detected._\n");
369
- } else {
370
- for (const fn of externalFuncs) {
371
- lines.push(`**\`${fnLabel(fn)}\`**`);
372
- for (const call of fn.externalCalls) lines.push(` - \`${call}\``);
373
- }
374
- lines.push("");
375
- }
376
-
377
- // Internal recursion
378
- lines.push("### Internal Recursion\n");
379
- const callMap = new Map(contract.functions.map((f) => [fnLabel(f), f.internalCalls]));
380
- const recursiveFns = contract.functions.filter((fn) => hasCycle(fnLabel(fn), fnLabel(fn), callMap, new Set()));
381
- if (recursiveFns.length === 0) {
382
- lines.push("_No recursive functions detected._\n");
383
- } else {
384
- for (const fn of recursiveFns) lines.push(`- **\`${fnLabel(fn)}\`** is recursive`);
385
- lines.push("");
386
- }
387
-
388
- // State variable touchpoints
389
- lines.push("### State Variable Touchpoints\n");
390
- const touchedFns = contract.functions.filter((f) => f.stateReads.length > 0 || f.stateWrites.length > 0);
391
- if (touchedFns.length === 0) {
392
- lines.push("_No state variable accesses detected._\n");
393
- } else {
394
- lines.push("| Function | Reads | Writes |");
395
- lines.push("|----------|-------|--------|");
396
- for (const fn of touchedFns) {
397
- const reads = fn.stateReads.map((r) => `\`${r}\``).join(", ");
398
- const writes = fn.stateWrites.map((w) => `\`${w}\``).join(", ");
399
- lines.push(`| \`${fnLabel(fn)}\` | ${reads} | ${writes} |`);
400
- }
401
- lines.push("");
402
- }
403
- }
404
-
405
- // External dependencies summary
406
- lines.push("---\n\n## External Dependencies\n");
407
-
408
- lines.push("### Import Paths\n");
409
- if (imports.length === 0) {
410
- lines.push("_No imports._\n");
411
- } else {
412
- for (const imp of imports) lines.push(`- \`${imp}\``);
413
- lines.push("");
414
- }
415
-
416
- const externalTargets = new Set<string>();
417
- for (const contract of contracts) {
418
- for (const fn of contract.functions) {
419
- for (const call of fn.externalCalls) {
420
- const target = call.split(".")[0];
421
- if (target && target !== "<expr>") externalTargets.add(target);
422
- }
423
- }
424
- }
425
-
426
- lines.push("### External Contract Interactions\n");
427
- if (externalTargets.size === 0) {
428
- lines.push("_No external contract interactions detected._\n");
429
- } else {
430
- for (const dep of externalTargets) lines.push(`- \`${dep}\``);
431
- lines.push("");
432
- }
433
-
434
- return lines.join("\n");
435
- };
436
-
437
- export const analyzeSolidityFile = async (soliditySource: string, mode: "full" | "short") => {
438
- let ast: any;
439
-
440
- try {
441
- ast = parse(soliditySource, { tolerant: true, loc: true, range: true });
442
- } catch (e: any) {
443
- return `# Parse Error\n\nFailed to parse Solidity source: ${e.message as string}`;
444
- }
445
-
446
- const imports: string[] = [];
447
- const contracts: ContractAnalysis[] = [];
448
-
449
- for (const node of ast.children ?? []) {
450
- if (node.type === "ImportDirective") {
451
- imports.push(node.path as string);
452
- }
453
- }
454
-
455
- for (const node of ast.children ?? []) {
456
- if (node.type !== "ContractDefinition") continue;
457
-
458
- const contract: ContractAnalysis = {
459
- name: node.name as string,
460
- kind: (node.kind as string) ?? "contract",
461
- baseContracts: (node.baseContracts ?? []).map(
462
- (bc: any) => (bc.baseName?.namePath ?? bc.baseName?.name ?? "?") as string,
463
- ),
464
- usingFor: [],
465
- stateVars: [],
466
- events: [],
467
- modifiers: [],
468
- functions: [],
469
- };
470
-
471
- const stateVarNames = new Set<string>();
472
-
473
- for (const member of node.subNodes ?? []) {
474
- switch (member.type) {
475
- case "StateVariableDeclaration":
476
- for (const v of member.variables ?? []) {
477
- stateVarNames.add(v.name as string);
478
- contract.stateVars.push({
479
- name: v.name as string,
480
- type: typeToString(v.typeName),
481
- visibility: (v.visibility as string) ?? "internal",
482
- constant: (v.isDeclaredConst as boolean) ?? false,
483
- immutable: (v.isImmutable as boolean) ?? false,
484
- });
485
- }
486
- break;
487
-
488
- case "EventDefinition": {
489
- const params = (member.parameters ?? []).map((p: any) => {
490
- const indexed = p.isIndexed ? "indexed " : "";
491
- const name = p.name ? ` ${p.name as string}` : "";
492
- return `${indexed}${typeToString(p.typeName)}${name}`;
493
- });
494
- contract.events.push({
495
- name: member.name as string,
496
- params,
497
- anonymous: (member.isAnonymous as boolean) ?? false,
498
- });
499
- break;
500
- }
501
-
502
- case "ModifierDefinition":
503
- contract.modifiers.push({
504
- name: member.name as string,
505
- params: (member.parameters ?? []).map(paramToString),
506
- });
507
- break;
508
-
509
- case "FunctionDefinition": {
510
- const { internalCalls, externalCalls, stateReads, stateWrites } = analyzeFunction(member, stateVarNames);
511
- contract.functions.push({
512
- name: (member.name as string) ?? "",
513
- isConstructor: (member.isConstructor as boolean) ?? false,
514
- isReceive: (member.isReceiveEther as boolean) ?? false,
515
- isFallback: (member.isFallback as boolean) ?? false,
516
- visibility: (member.visibility as string) ?? "internal",
517
- mutability: (member.stateMutability as string) ?? "nonpayable",
518
- params: (member.parameters ?? []).map(paramToString),
519
- returns: (member.returnParameters ?? []).map(paramToString),
520
- modifiers: (member.modifiers ?? []).map((m: any) => m.name as string),
521
- internalCalls,
522
- externalCalls,
523
- stateReads,
524
- stateWrites,
525
- });
526
- break;
527
- }
528
-
529
- case "UsingForDeclaration": {
530
- const forType = member.typeName ? typeToString(member.typeName) : "*";
531
- if (member.libraryName) {
532
- contract.usingFor.push(`\`${member.libraryName as string}\` for \`${forType}\``);
533
- } else {
534
- // New-style: using {fn1, fn2, ...} for T
535
- const fns = (member.functions ?? [])
536
- .map((f: any) => (f.typeName?.namePath ?? f.typeName?.name ?? f.path ?? "?") as string)
537
- .join(", ");
538
- contract.usingFor.push(`{${fns}} for \`${forType}\``);
539
- }
540
- break;
541
- }
542
- }
543
- }
544
-
545
- contracts.push(contract);
546
- }
547
-
548
- return mode === "short" ? generateShortMarkdown(imports, contracts) : generateMarkdown(imports, contracts);
549
- };
550
-
551
- export const solidityAnalyzerTool = tool(
552
- async ({ solidityFile, mode }) => {
553
- return analyzeSolidityFile(solidityFile, mode);
554
- },
555
- {
556
- name: "solidity_analyzer",
557
- description:
558
- "Parse a Solidity source file and generate a markdown report. Use mode='short' for a compact token-efficient summary (imports, state, modifiers, events, function signatures, external calls). Use mode='full' for the complete report including storage layout table, call graph, recursion detection, state variable touchpoints, and external dependencies.",
559
- schema: z.object({
560
- solidityFile: z.string().describe("The full Solidity source code to analyze."),
561
- mode: z
562
- .enum(["full", "short"])
563
- .default("full")
564
- .describe("Report verbosity. 'short' saves tokens; 'full' provides the complete analysis."),
565
- }),
566
- },
567
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/agents/auditor/tools/solidity-analyzer/tool.ts ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { parse } from "@solidity-parser/parser";
2
+ import { tool } from "langchain";
3
+ import { z } from "zod";
4
+
5
+ import {
6
+ analyzeFunction,
7
+ buildCommentBlocks,
8
+ type ContractAnalysis,
9
+ extractSolcVersion,
10
+ findCommentFor,
11
+ generateBriefMarkdown,
12
+ generateFullMarkdown,
13
+ paramToString,
14
+ type RenderOptions,
15
+ typeToString,
16
+ } from "./utils.ts";
17
+
18
+ export const analyzeSolidityFile = async (
19
+ soliditySource: string,
20
+ mode: "full" | "short",
21
+ filePath?: string,
22
+ importance?: number,
23
+ ): Promise<string> => {
24
+ let ast: any;
25
+ try {
26
+ ast = parse(soliditySource, { tolerant: true, loc: true, range: true });
27
+ } catch (e: any) {
28
+ return `# Parse Error\n\nFailed to parse Solidity source: ${e.message as string}`;
29
+ }
30
+
31
+ const comments = buildCommentBlocks(soliditySource);
32
+ const imports: string[] = [];
33
+ const contracts: ContractAnalysis[] = [];
34
+
35
+ for (const node of ast.children ?? []) {
36
+ if (node.type === "ImportDirective") imports.push(node.path as string);
37
+ }
38
+
39
+ for (const node of ast.children ?? []) {
40
+ if (node.type !== "ContractDefinition") continue;
41
+
42
+ const contractComment = node.loc ? findCommentFor(node.loc.start.line, comments) : undefined;
43
+
44
+ const contract: ContractAnalysis = {
45
+ name: node.name as string,
46
+ kind: (node.kind as string) ?? "contract",
47
+ baseContracts: (node.baseContracts ?? []).map(
48
+ (bc: any) => (bc.baseName?.namePath ?? bc.baseName?.name ?? "?") as string,
49
+ ),
50
+ usingFor: [],
51
+ stateVars: [],
52
+ events: [],
53
+ errors: [],
54
+ modifiers: [],
55
+ functions: [],
56
+ natspec: contractComment?.natspec,
57
+ };
58
+
59
+ const stateVarNames = new Set<string>();
60
+
61
+ for (const member of node.subNodes ?? []) {
62
+ const memberComment = member.loc ? findCommentFor(member.loc.start.line, comments) : undefined;
63
+
64
+ switch (member.type) {
65
+ case "StateVariableDeclaration":
66
+ for (const v of member.variables ?? []) {
67
+ stateVarNames.add(v.name as string);
68
+ contract.stateVars.push({
69
+ name: v.name as string,
70
+ type: typeToString(v.typeName),
71
+ visibility: (v.visibility as string) ?? "internal",
72
+ constant: (v.isDeclaredConst as boolean) ?? false,
73
+ immutable: (v.isImmutable as boolean) ?? false,
74
+ natspec: memberComment?.natspec,
75
+ });
76
+ }
77
+ break;
78
+
79
+ case "EventDefinition": {
80
+ const params = (member.parameters ?? []).map((p: any) => {
81
+ const indexed = p.isIndexed ? "indexed " : "";
82
+ const name = p.name ? ` ${p.name as string}` : "";
83
+ return `${indexed}${typeToString(p.typeName)}${name}`;
84
+ });
85
+ contract.events.push({
86
+ name: member.name as string,
87
+ params,
88
+ anonymous: (member.isAnonymous as boolean) ?? false,
89
+ natspec: memberComment?.natspec,
90
+ });
91
+ break;
92
+ }
93
+
94
+ case "CustomErrorDefinition":
95
+ contract.errors.push({
96
+ name: member.name as string,
97
+ params: (member.parameters ?? []).map((p: any) => paramToString(p)),
98
+ natspec: memberComment?.natspec,
99
+ });
100
+ break;
101
+
102
+ case "ModifierDefinition":
103
+ contract.modifiers.push({
104
+ name: member.name as string,
105
+ params: (member.parameters ?? []).map(paramToString),
106
+ natspec: memberComment?.natspec,
107
+ });
108
+ break;
109
+
110
+ case "FunctionDefinition": {
111
+ const { internalCalls, externalCalls, stateReads, stateWrites } = analyzeFunction(member, stateVarNames);
112
+ contract.functions.push({
113
+ name: (member.name as string) ?? "",
114
+ isConstructor: (member.isConstructor as boolean) ?? false,
115
+ isReceive: (member.isReceiveEther as boolean) ?? false,
116
+ isFallback: (member.isFallback as boolean) ?? false,
117
+ visibility: (member.visibility as string) ?? "internal",
118
+ mutability: (member.stateMutability as string) ?? "nonpayable",
119
+ params: (member.parameters ?? []).map(paramToString),
120
+ returns: (member.returnParameters ?? []).map(paramToString),
121
+ modifiers: (member.modifiers ?? []).map((m: any) => m.name as string),
122
+ internalCalls,
123
+ externalCalls,
124
+ stateReads,
125
+ stateWrites,
126
+ natspec: memberComment?.natspec,
127
+ });
128
+ break;
129
+ }
130
+
131
+ case "UsingForDeclaration": {
132
+ const forType = member.typeName ? typeToString(member.typeName) : "*";
133
+ if (member.libraryName) {
134
+ contract.usingFor.push(`\`${member.libraryName as string}\` for \`${forType}\``);
135
+ } else {
136
+ const fns = (member.functions ?? [])
137
+ .map((f: any) => (f.typeName?.namePath ?? f.typeName?.name ?? f.path ?? "?") as string)
138
+ .join(", ");
139
+ contract.usingFor.push(`{${fns}} for \`${forType}\``);
140
+ }
141
+ break;
142
+ }
143
+ }
144
+ }
145
+
146
+ contracts.push(contract);
147
+ }
148
+
149
+ const opts: RenderOptions = {
150
+ filePath,
151
+ importance,
152
+ lineCount: soliditySource.split("\n").length,
153
+ solcVersion: extractSolcVersion(soliditySource),
154
+ };
155
+
156
+ return mode === "short"
157
+ ? generateBriefMarkdown(imports, contracts, opts)
158
+ : generateFullMarkdown(imports, contracts, opts);
159
+ };
160
+
161
+ export const solidityAnalyzerTool = tool(async ({ solidityFile, mode }) => analyzeSolidityFile(solidityFile, mode), {
162
+ name: "solidity_analyzer",
163
+ description:
164
+ "Parse a Solidity source file and generate a markdown report. Use mode='short' for a compact summary (meta, external calls, function table). Use mode='full' for the complete report including storage, events, errors, per-function call graph, recursion detection, and state variable touchpoints.",
165
+ schema: z.object({
166
+ solidityFile: z.string().describe("The full Solidity source code to analyze."),
167
+ mode: z
168
+ .enum(["full", "short"])
169
+ .default("full")
170
+ .describe("Report verbosity. 'short' saves tokens; 'full' provides the complete analysis."),
171
+ }),
172
+ });
src/agents/auditor/tools/solidity-analyzer/utils.ts ADDED
@@ -0,0 +1,520 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { visit } from "@solidity-parser/parser";
2
+
3
+ const ASSIGNMENT_OPS = new Set(["=", "+=", "-=", "*=", "/=", "%=", "|=", "&=", "^=", "<<=", ">>=", "**="]);
4
+ const BUILTIN_NAMESPACES = new Set(["abi", "block", "msg", "tx", "bytes", "string", "type"]);
5
+
6
+ export interface NatSpec {
7
+ title?: string;
8
+ author?: string;
9
+ notice?: string;
10
+ dev?: string;
11
+ params: Record<string, string>;
12
+ returns: string[];
13
+ inheritdoc?: string;
14
+ custom: Record<string, string>;
15
+ }
16
+
17
+ export interface ParsedComment {
18
+ text: string;
19
+ startLine: number;
20
+ endLine: number;
21
+ isNatSpec: boolean;
22
+ natspec?: NatSpec;
23
+ }
24
+
25
+ export interface StateVar {
26
+ name: string;
27
+ type: string;
28
+ visibility: string;
29
+ constant: boolean;
30
+ immutable: boolean;
31
+ natspec?: NatSpec;
32
+ }
33
+
34
+ export interface EventDef {
35
+ name: string;
36
+ params: string[];
37
+ anonymous: boolean;
38
+ natspec?: NatSpec;
39
+ }
40
+
41
+ export interface ErrorDef {
42
+ name: string;
43
+ params: string[];
44
+ natspec?: NatSpec;
45
+ }
46
+
47
+ export interface ModifierDef {
48
+ name: string;
49
+ params: string[];
50
+ natspec?: NatSpec;
51
+ }
52
+
53
+ export interface FunctionDef {
54
+ name: string;
55
+ isConstructor: boolean;
56
+ isReceive: boolean;
57
+ isFallback: boolean;
58
+ visibility: string;
59
+ mutability: string;
60
+ params: string[];
61
+ returns: string[];
62
+ modifiers: string[];
63
+ internalCalls: string[];
64
+ externalCalls: string[];
65
+ stateReads: string[];
66
+ stateWrites: string[];
67
+ natspec?: NatSpec;
68
+ }
69
+
70
+ export interface ContractAnalysis {
71
+ name: string;
72
+ kind: string;
73
+ baseContracts: string[];
74
+ usingFor: string[];
75
+ stateVars: StateVar[];
76
+ events: EventDef[];
77
+ errors: ErrorDef[];
78
+ modifiers: ModifierDef[];
79
+ functions: FunctionDef[];
80
+ natspec?: NatSpec;
81
+ }
82
+
83
+ export interface RenderOptions {
84
+ filePath?: string;
85
+ importance?: number;
86
+ lineCount: number;
87
+ solcVersion: string;
88
+ }
89
+
90
+ export const extractSolcVersion = (source: string): string => {
91
+ const match = source.match(/pragma\s+solidity\s+([^;]+);/);
92
+ return match ? match[1].trim() : "—";
93
+ };
94
+
95
+ const parseNatSpecTags = (text: string): NatSpec => {
96
+ const result: NatSpec = { params: {}, returns: [], custom: {} };
97
+
98
+ const firstTag = text.search(/@(?:title|author|notice|dev|param|return|inheritdoc|custom:)/);
99
+ if (firstTag > 0) {
100
+ const implicit = text.slice(0, firstTag).trim();
101
+ if (implicit) result.notice = implicit.replace(/\n\s*/g, " ");
102
+ } else if (firstTag === -1 && text.trim()) {
103
+ result.notice = text.trim().replace(/\n\s*/g, " ");
104
+ }
105
+
106
+ const tagRe = /@(custom:\S+|\w+)([^@]*)/g;
107
+ let m: RegExpExecArray | null;
108
+ while ((m = tagRe.exec(text)) !== null) {
109
+ const tag = m[1];
110
+ const value = m[2].trim().replace(/\n\s*/g, " ");
111
+ if (tag === "title") result.title = value;
112
+ else if (tag === "author") result.author = value;
113
+ else if (tag === "notice") result.notice = value;
114
+ else if (tag === "dev") result.dev = value;
115
+ else if (tag === "inheritdoc") result.inheritdoc = value;
116
+ else if (tag === "param") {
117
+ const sp = value.indexOf(" ");
118
+ if (sp > 0) result.params[value.slice(0, sp)] = value.slice(sp + 1);
119
+ else if (value) result.params[value] = "";
120
+ } else if (tag === "return") result.returns.push(value);
121
+ else if (tag.startsWith("custom:")) result.custom[tag.slice(7)] = value;
122
+ }
123
+
124
+ return result;
125
+ };
126
+
127
+ export const buildCommentBlocks = (source: string): ParsedComment[] => {
128
+ const blocks: ParsedComment[] = [];
129
+ const lines = source.split("\n");
130
+ let i = 0;
131
+
132
+ while (i < lines.length) {
133
+ const raw = lines[i];
134
+ const trimmed = raw.trimStart();
135
+
136
+ if (trimmed.startsWith("///")) {
137
+ const startLine = i + 1;
138
+ const texts: string[] = [];
139
+ while (i < lines.length && lines[i].trimStart().startsWith("///")) {
140
+ texts.push(lines[i].trimStart().slice(3).replace(/^ /, ""));
141
+ i++;
142
+ }
143
+ const text = texts.join("\n");
144
+ blocks.push({ text, startLine, endLine: i, isNatSpec: true, natspec: parseNatSpecTags(text) });
145
+ continue;
146
+ }
147
+
148
+ const mlStart = raw.indexOf("/*");
149
+ if (mlStart !== -1) {
150
+ const isNatSpec = raw[mlStart + 2] === "*" && raw[mlStart + 3] !== "/";
151
+ const startLine = i + 1;
152
+ const closeOnSame = raw.indexOf("*/", mlStart + 2);
153
+
154
+ if (closeOnSame !== -1) {
155
+ const inner = raw.slice(mlStart + (isNatSpec ? 3 : 2), closeOnSame).trim();
156
+ blocks.push({
157
+ text: inner,
158
+ startLine,
159
+ endLine: startLine,
160
+ isNatSpec,
161
+ natspec: isNatSpec ? parseNatSpecTags(inner) : undefined,
162
+ });
163
+ i++;
164
+ continue;
165
+ }
166
+
167
+ const rawLines: string[] = [raw.slice(mlStart + (isNatSpec ? 3 : 2))];
168
+ i++;
169
+ while (i < lines.length) {
170
+ const closeIdx = lines[i].indexOf("*/");
171
+ if (closeIdx !== -1) {
172
+ rawLines.push(lines[i].slice(0, closeIdx));
173
+ i++;
174
+ break;
175
+ }
176
+ rawLines.push(lines[i]);
177
+ i++;
178
+ }
179
+ const text = rawLines
180
+ .map((l) => l.replace(/^\s*\*\s?/, ""))
181
+ .join("\n")
182
+ .trim();
183
+ blocks.push({ text, startLine, endLine: i, isNatSpec, natspec: isNatSpec ? parseNatSpecTags(text) : undefined });
184
+ continue;
185
+ }
186
+
187
+ if (trimmed.startsWith("//")) {
188
+ blocks.push({ text: trimmed.slice(2).trim(), startLine: i + 1, endLine: i + 1, isNatSpec: false });
189
+ }
190
+
191
+ i++;
192
+ }
193
+
194
+ return blocks;
195
+ };
196
+
197
+ export const findCommentFor = (line: number, comments: ParsedComment[]): ParsedComment | undefined =>
198
+ comments.find((c) => c.endLine === line - 1) ?? comments.find((c) => c.startLine === line && !c.isNatSpec);
199
+
200
+ export const typeToString = (node: any): string => {
201
+ if (!node) return "unknown";
202
+ switch (node.type) {
203
+ case "ElementaryTypeName":
204
+ return node.name as string;
205
+ case "UserDefinedTypeName":
206
+ return (node.namePath ?? node.name) as string;
207
+ case "ArrayTypeName":
208
+ return `${typeToString(node.baseTypeName)}[${node.length ?? ""}]`;
209
+ case "Mapping":
210
+ return `mapping(${typeToString(node.keyType)} => ${typeToString(node.valueType)})`;
211
+ case "FunctionTypeName":
212
+ return "function";
213
+ default:
214
+ return "unknown";
215
+ }
216
+ };
217
+
218
+ export const paramToString = (p: any): string => {
219
+ if (!p) return "?";
220
+ const type = typeToString(p.typeName);
221
+ return p.name ? `${type} ${p.name}` : type;
222
+ };
223
+
224
+ const collectLHSRoots = (node: any, targets: Set<string>) => {
225
+ if (!node) return;
226
+ switch (node.type) {
227
+ case "Identifier":
228
+ targets.add(node.name as string);
229
+ break;
230
+ case "MemberAccess":
231
+ collectLHSRoots(node.expression, targets);
232
+ break;
233
+ case "IndexAccess":
234
+ collectLHSRoots(node.base, targets);
235
+ break;
236
+ case "TupleExpression":
237
+ for (const c of node.components ?? []) collectLHSRoots(c, targets);
238
+ break;
239
+ }
240
+ };
241
+
242
+ export const analyzeFunction = (funcNode: any, stateVarNames: Set<string>) => {
243
+ const internalCalls = new Set<string>();
244
+ const externalCalls = new Set<string>();
245
+ const writeTargets = new Set<string>();
246
+ const allStateAccesses = new Set<string>();
247
+ const localVars = new Set<string>();
248
+
249
+ if (!funcNode.body) {
250
+ return { internalCalls: [], externalCalls: [], stateReads: [], stateWrites: [] };
251
+ }
252
+
253
+ for (const p of funcNode.parameters ?? []) {
254
+ if (p?.name) localVars.add(p.name as string);
255
+ }
256
+ for (const p of funcNode.returnParameters ?? []) {
257
+ if (p?.name) localVars.add(p.name as string);
258
+ }
259
+
260
+ visit(funcNode.body, {
261
+ VariableDeclarationStatement: (node: any) => {
262
+ for (const v of node.variables ?? []) {
263
+ if (v?.name) localVars.add(v.name as string);
264
+ }
265
+ },
266
+ });
267
+
268
+ const effectiveStateVars = new Set([...stateVarNames].filter((v) => !localVars.has(v)));
269
+
270
+ visit(funcNode.body, {
271
+ ExpressionStatement: (node: any) => {
272
+ const expr = node.expression;
273
+ if (expr?.type === "BinaryOperation" && ASSIGNMENT_OPS.has(expr.operator as string)) {
274
+ collectLHSRoots(expr.left, writeTargets);
275
+ }
276
+ if (
277
+ expr?.type === "UnaryOperation" &&
278
+ (expr.operator === "++" || expr.operator === "--" || expr.operator === "delete")
279
+ ) {
280
+ collectLHSRoots(expr.subExpression, writeTargets);
281
+ }
282
+ },
283
+ });
284
+
285
+ visit(funcNode.body, {
286
+ FunctionCall: (node: any) => {
287
+ const expr = node.expression;
288
+ if (expr?.type === "Identifier") {
289
+ internalCalls.add(expr.name as string);
290
+ } else if (expr?.type === "MemberAccess") {
291
+ const base = expr.expression;
292
+ if (base?.type === "Identifier" && (base.name === "this" || base.name === "super")) {
293
+ internalCalls.add(expr.memberName as string);
294
+ } else if (base?.type === "Identifier" && BUILTIN_NAMESPACES.has(base.name as string)) {
295
+ // builtin namespace — skip
296
+ } else {
297
+ const baseStr = base?.type === "Identifier" ? (base.name as string) : "<expr>";
298
+ externalCalls.add(`${baseStr}.${expr.memberName as string}`);
299
+ }
300
+ }
301
+ },
302
+ Identifier: (node: any) => {
303
+ if (effectiveStateVars.has(node.name as string)) {
304
+ allStateAccesses.add(node.name as string);
305
+ }
306
+ },
307
+ });
308
+
309
+ const stateWrites = [...allStateAccesses].filter((v) => writeTargets.has(v));
310
+
311
+ return {
312
+ internalCalls: [...internalCalls],
313
+ externalCalls: [...externalCalls],
314
+ stateReads: [...allStateAccesses],
315
+ stateWrites,
316
+ };
317
+ };
318
+
319
+ export const hasCycle = (
320
+ start: string,
321
+ current: string,
322
+ callMap: Map<string, string[]>,
323
+ visited: Set<string>,
324
+ ): boolean => {
325
+ for (const callee of callMap.get(current) ?? []) {
326
+ if (callee === start) return true;
327
+ if (!visited.has(callee)) {
328
+ visited.add(callee);
329
+ if (hasCycle(start, callee, callMap, visited)) return true;
330
+ }
331
+ }
332
+ return false;
333
+ };
334
+
335
+ export const fnLabel = (fn: FunctionDef): string => {
336
+ if (fn.isConstructor) return "constructor";
337
+ if (fn.isReceive) return "receive";
338
+ if (fn.isFallback) return "fallback";
339
+ return fn.name;
340
+ };
341
+
342
+ const renderNatSpec = (ns: NatSpec | undefined): string => {
343
+ if (!ns) return "—";
344
+ const parts: string[] = [];
345
+ if (ns.title) parts.push(`@title "${ns.title}"`);
346
+ if (ns.author) parts.push(`@author "${ns.author}"`);
347
+ if (ns.notice) parts.push(`@notice "${ns.notice}"`);
348
+ if (ns.dev) parts.push(`@dev "${ns.dev}"`);
349
+ for (const [k, v] of Object.entries(ns.params)) {
350
+ parts.push(v ? `@param ${k}: "${v}"` : `@param ${k}`);
351
+ }
352
+ for (const r of ns.returns) {
353
+ if (r) parts.push(`@return "${r}"`);
354
+ }
355
+ return parts.length > 0 ? parts.join(" · ") : "—";
356
+ };
357
+
358
+ const renderContractFull = (contract: ContractAnalysis, imports: string[], lines: string[]) => {
359
+ lines.push("## Meta");
360
+ const inherits = contract.baseContracts.length > 0 ? `[${contract.baseContracts.join(", ")}]` : "—";
361
+ lines.push(`- kind: ${contract.kind} · inherits: ${inherits}`);
362
+ if (contract.usingFor.length > 0) lines.push(`- uses: [${contract.usingFor.join(", ")}]`);
363
+ lines.push(`- imports: ${imports.length > 0 ? imports.map((i) => `\`${i}\``).join(", ") : "—"}`);
364
+ lines.push(`- docs: ${renderNatSpec(contract.natspec)}`);
365
+ lines.push("");
366
+
367
+ if (contract.stateVars.length > 0) {
368
+ lines.push("## Storage");
369
+ lines.push("| name | type | vis | flags | desc |");
370
+ lines.push("|------|------|-----|-------|------|");
371
+ for (const v of contract.stateVars) {
372
+ const flags = [v.constant && "constant", v.immutable && "immutable"].filter(Boolean).join(", ") || "—";
373
+ const desc = v.natspec?.notice ?? v.natspec?.dev ?? "—";
374
+ lines.push(`| \`${v.name}\` | \`${v.type}\` | ${v.visibility} | ${flags} | ${desc} |`);
375
+ }
376
+ lines.push("");
377
+ }
378
+
379
+ if (contract.events.length > 0) {
380
+ lines.push("## Events");
381
+ for (const e of contract.events) {
382
+ const notice = e.natspec?.notice ? ` — ${e.natspec.notice}` : "";
383
+ lines.push(`- \`${e.name}(${e.params.join(", ")})\`${e.anonymous ? " _(anon)_" : ""}${notice}`);
384
+ }
385
+ lines.push("");
386
+ }
387
+
388
+ if (contract.errors.length > 0) {
389
+ lines.push("## Errors");
390
+ for (const e of contract.errors) lines.push(`- \`${e.name}(${e.params.join(", ")})\``);
391
+ lines.push("");
392
+ }
393
+
394
+ if (contract.modifiers.length > 0) {
395
+ lines.push("## Modifiers");
396
+ for (const m of contract.modifiers) {
397
+ const notice = m.natspec?.notice ? ` — ${m.natspec.notice}` : "";
398
+ lines.push(`- \`${m.name}(${m.params.join(", ")})\`${notice}`);
399
+ }
400
+ lines.push("");
401
+ }
402
+
403
+ const allExternalCalls = new Set(contract.functions.flatMap((f) => f.externalCalls));
404
+ if (allExternalCalls.size > 0) {
405
+ lines.push("## External Calls");
406
+ for (const call of allExternalCalls) lines.push(`- \`${call}\``);
407
+ lines.push("");
408
+ }
409
+
410
+ if (contract.functions.length > 0) {
411
+ lines.push("## Functions");
412
+ lines.push("");
413
+
414
+ const callMap = new Map(contract.functions.map((f) => [fnLabel(f), f.internalCalls]));
415
+
416
+ for (const fn of contract.functions) {
417
+ const label = fnLabel(fn);
418
+ lines.push(`### ${label}`);
419
+
420
+ const modsStr = fn.modifiers.length > 0 ? ` · modifiers: [${fn.modifiers.join(", ")}]` : "";
421
+ lines.push(`- visibility: ${fn.visibility} · mutability: ${fn.mutability}${modsStr}`);
422
+
423
+ const paramsStr = fn.params.length > 0 ? fn.params.join(", ") : "—";
424
+ const returnsStr = fn.returns.length > 0 ? `\`${fn.returns.join(", ")}\`` : "—";
425
+ lines.push(`- parameters: \`(${paramsStr})\` · returns: ${returnsStr}`);
426
+
427
+ if (fn.externalCalls.length > 0) {
428
+ lines.push(`- calls: [${fn.externalCalls.map((c) => `\`${c}\``).join(", ")}]`);
429
+ }
430
+ if (fn.internalCalls.length > 0) {
431
+ lines.push(`- graph: \`${fn.internalCalls.map((c) => `${label} → ${c}`).join(", ")}\``);
432
+ }
433
+
434
+ lines.push(`- recurse: ${hasCycle(label, label, callMap, new Set()) ? "yes ⚠" : "no"}`);
435
+
436
+ if (fn.stateReads.length > 0 || fn.stateWrites.length > 0) {
437
+ const reads = fn.stateReads.length > 0 ? fn.stateReads.map((r) => `\`${r}\``).join(", ") : "—";
438
+ const writes = fn.stateWrites.length > 0 ? fn.stateWrites.map((w) => `\`${w}\``).join(", ") : "—";
439
+ lines.push(`- state: reads [${reads}] · writes [${writes}]`);
440
+ }
441
+
442
+ lines.push(`- docs: ${renderNatSpec(fn.natspec)}`);
443
+ lines.push("");
444
+ }
445
+ }
446
+ };
447
+
448
+ const renderContractBrief = (contract: ContractAnalysis, imports: string[], lines: string[]) => {
449
+ lines.push("## Meta");
450
+ const inherits = contract.baseContracts.length > 0 ? `[${contract.baseContracts.join(", ")}]` : "—";
451
+ const importsList = imports.length > 0 ? imports.map((i) => `\`${i}\``).join(", ") : "—";
452
+ lines.push(`- kind: ${contract.kind} · inherits: ${inherits}`);
453
+ lines.push(`- imports: ${importsList}`);
454
+ lines.push(`- docs: ${renderNatSpec(contract.natspec)}`);
455
+ lines.push("");
456
+
457
+ const allExternalCalls = new Set(contract.functions.flatMap((f) => f.externalCalls));
458
+ if (allExternalCalls.size > 0) {
459
+ lines.push("## External Calls");
460
+ lines.push([...allExternalCalls].map((c) => `\`${c}\``).join(" · "));
461
+ lines.push("");
462
+ }
463
+
464
+ if (contract.functions.length > 0) {
465
+ lines.push("## Functions");
466
+ lines.push("| function | visibility | mutability | parameters | returns | modifiers |");
467
+ lines.push("|----------|------------|------------|------------|---------|-----------|");
468
+ for (const fn of contract.functions) {
469
+ const label = fnLabel(fn);
470
+ const params = fn.params.length > 0 ? fn.params.join(", ") : "—";
471
+ const returns = fn.returns.length > 0 ? fn.returns.join(", ") : "—";
472
+ const mods = fn.modifiers.length > 0 ? fn.modifiers.join(", ") : "—";
473
+ lines.push(`| \`${label}\` | ${fn.visibility} | ${fn.mutability} | ${params} | ${returns} | ${mods} |`);
474
+ }
475
+ lines.push("");
476
+ }
477
+ };
478
+
479
+ const fileHeader = (contracts: ContractAnalysis[], mode: "full" | "short", opts: RenderOptions): string[] => {
480
+ const names = contracts.map((c) => c.name).join(", ");
481
+ const label = mode === "full" ? "FULL" : "BRIEF";
482
+ const rankStr = opts.importance !== undefined ? ` | importance: ${opts.importance}/5` : "";
483
+ return [
484
+ `# ${names} · ${label}`,
485
+ `> path: \`${opts.filePath ?? "—"}\` | lines: ${opts.lineCount} | solc: ${opts.solcVersion}${rankStr}`,
486
+ "",
487
+ ];
488
+ };
489
+
490
+ export const generateFullMarkdown = (imports: string[], contracts: ContractAnalysis[], opts: RenderOptions): string => {
491
+ const lines: string[] = fileHeader(contracts, "full", opts);
492
+
493
+ for (let i = 0; i < contracts.length; i++) {
494
+ if (contracts.length > 1) {
495
+ if (i > 0) lines.push("---", "");
496
+ lines.push(`## ◆ ${contracts[i].name}`, "");
497
+ }
498
+ renderContractFull(contracts[i], imports, lines);
499
+ }
500
+
501
+ return lines.join("\n");
502
+ };
503
+
504
+ export const generateBriefMarkdown = (
505
+ imports: string[],
506
+ contracts: ContractAnalysis[],
507
+ opts: RenderOptions,
508
+ ): string => {
509
+ const lines: string[] = fileHeader(contracts, "short", opts);
510
+
511
+ for (let i = 0; i < contracts.length; i++) {
512
+ if (contracts.length > 1) {
513
+ if (i > 0) lines.push("---", "");
514
+ lines.push(`## ◆ ${contracts[i].name}`, "");
515
+ }
516
+ renderContractBrief(contracts[i], imports, lines);
517
+ }
518
+
519
+ return lines.join("\n");
520
+ };
src/agents/auditor/utils.ts CHANGED
@@ -1,3 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  export const matchLines = (fileContent: string, codeSnippet: string): string | null => {
2
  const fileLines = fileContent.split("\n");
3
  const snippetLines = codeSnippet.split("\n").map((line) => line.trim());
@@ -34,3 +69,49 @@ export const matchLines = (fileContent: string, codeSnippet: string): string | n
34
 
35
  return null;
36
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { DOC_BASENAMES, DOC_EXTS, MAX_DEPTH, SKIP_DIRS, SOL_EXT, SOL_TEST_SUFFIXES } from "./config.ts";
5
+
6
+ export const walkDirectory = (dir: string, depth: number, solFiles: string[], docFiles: string[]) => {
7
+ if (depth > MAX_DEPTH) return;
8
+
9
+ let entries: fs.Dirent[];
10
+ try {
11
+ entries = fs.readdirSync(dir, { withFileTypes: true });
12
+ } catch {
13
+ return;
14
+ }
15
+
16
+ for (const entry of entries) {
17
+ if (entry.isDirectory()) {
18
+ if (!SKIP_DIRS.has(entry.name)) {
19
+ walkDirectory(path.join(dir, entry.name), depth + 1, solFiles, docFiles);
20
+ }
21
+ } else if (entry.isFile()) {
22
+ const fullPath = path.join(dir, entry.name);
23
+ const ext = path.extname(entry.name).toLowerCase();
24
+ const base = path.basename(entry.name, ext).toLowerCase();
25
+
26
+ if (ext === SOL_EXT) {
27
+ const isTest = SOL_TEST_SUFFIXES.some((suffix) => entry.name.endsWith(suffix));
28
+ if (!isTest) solFiles.push(fullPath);
29
+ } else if (DOC_EXTS.has(ext) || DOC_BASENAMES.has(base)) {
30
+ docFiles.push(fullPath);
31
+ }
32
+ }
33
+ }
34
+ };
35
+
36
  export const matchLines = (fileContent: string, codeSnippet: string): string | null => {
37
  const fileLines = fileContent.split("\n");
38
  const snippetLines = codeSnippet.split("\n").map((line) => line.trim());
 
69
 
70
  return null;
71
  };
72
+
73
+ type ReviewEntry = {
74
+ finding: {
75
+ title: string;
76
+ severity: string;
77
+ location: string;
78
+ description: string;
79
+ codeSnippet: string;
80
+ };
81
+ review: {
82
+ isFalsePositive: boolean;
83
+ confidence: number;
84
+ review: string;
85
+ exploitablePaths: string[];
86
+ };
87
+ };
88
+
89
+ export const buildReviewBlocks = (fileEntries: ReviewEntry[], iterationCount: number): string => {
90
+ const blocks = fileEntries.map(({ finding, review }, i) => {
91
+ const verdict = review.isFalsePositive
92
+ ? `FALSO POSITIVO (confiança: ${review.confidence}/100)`
93
+ : `VERDADEIRO POSITIVO (confiança: ${review.confidence}/100)`;
94
+ const pathsLabel = review.isFalsePositive ? "Razão de Bloqueio" : "Caminhos de Exploração";
95
+ const pathsContent =
96
+ review.exploitablePaths.length > 0
97
+ ? review.exploitablePaths.map((p) => ` - ${p}`).join("\n")
98
+ : " (nenhum fornecido)";
99
+
100
+ return `[Achado ${i + 1}/${fileEntries.length}] ${finding.title}
101
+ Severidade: ${finding.severity}
102
+ Localização: linhas ${finding.location}
103
+ Descrição: ${finding.description}
104
+
105
+ Código:
106
+ \`\`\`solidity
107
+ ${finding.codeSnippet}
108
+ \`\`\`
109
+
110
+ Veredito do Revisor: ${verdict}
111
+ Análise do Revisor: ${review.review}
112
+ ${pathsLabel}:
113
+ ${pathsContent}`;
114
+ });
115
+
116
+ return `=== Iteração ${iterationCount} — Achados e Revisões do Especialista (${fileEntries.length} achado(s)) ===\n\n${blocks.join("\n\n---\n\n")}`;
117
+ };
src/config/llm.ts CHANGED
@@ -5,32 +5,39 @@ import type { BaseChatModel } from "@langchain/core/language_models/chat_models"
5
 
6
  export type LLMProvider = "google" | "openrouter" | "anthropic";
7
 
8
- export function createLLM(overrideProvider?: LLMProvider): BaseChatModel {
 
 
 
 
 
 
9
  const provider = overrideProvider || (process.env.LLM_PROVIDER as LLMProvider) || "openrouter";
10
 
11
  switch (provider) {
12
  case "openrouter":
13
  return new ChatOpenRouter({
14
- model: process.env.OPENROUTER_MODEL || "google/gemini-3.1-flash-lite",
15
- temperature: 0.2,
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",
24
- temperature: 0.2,
25
- maxTokens: 4096,
 
26
  });
 
27
  case "google":
28
  default:
29
  return new ChatGoogleGenerativeAI({
30
  apiKey: process.env.GOOGLE_API_KEY || "",
31
- model: process.env.MODEL_NAME || "gemini-2.5-flash",
32
- temperature: 0.2,
33
- maxOutputTokens: 4096,
34
  });
35
  }
36
  }
 
5
 
6
  export type LLMProvider = "google" | "openrouter" | "anthropic";
7
 
8
+ export interface LLMOptions {
9
+ model?: string;
10
+ temperature?: number | null;
11
+ maxTokens?: number;
12
+ }
13
+
14
+ export function createLLM(overrideProvider?: LLMProvider, options?: LLMOptions): BaseChatModel {
15
  const provider = overrideProvider || (process.env.LLM_PROVIDER as LLMProvider) || "openrouter";
16
 
17
  switch (provider) {
18
  case "openrouter":
19
  return new ChatOpenRouter({
20
+ model: options?.model || process.env.OPENROUTER_MODEL || "google/gemini-3.1-flash-lite",
21
+ ...(options?.temperature !== null && { temperature: options?.temperature ?? 0.2 }),
22
  apiKey: process.env.OPENROUTER_API_KEY,
23
+ maxTokens: options?.maxTokens ?? 4096,
24
  });
25
 
 
26
  case "anthropic":
27
  return new ChatAnthropic({
28
+ model: options?.model || process.env.ANTHROPIC_MODEL || "claude-sonnet-4-6",
29
+ ...(options?.temperature !== null && { temperature: options?.temperature ?? 0.2 }),
30
+ apiKey: process.env.ANTHROPIC_API_KEY,
31
+ maxTokens: options?.maxTokens ?? 4096,
32
  });
33
+
34
  case "google":
35
  default:
36
  return new ChatGoogleGenerativeAI({
37
  apiKey: process.env.GOOGLE_API_KEY || "",
38
+ model: options?.model || process.env.MODEL_NAME || "gemini-2.5-flash",
39
+ ...(options?.temperature !== null && { temperature: options?.temperature ?? 0.2 }),
40
+ maxOutputTokens: options?.maxTokens ?? 4096,
41
  });
42
  }
43
  }