Uanderson Silva commited on
Commit
10be466
Β·
1 Parent(s): ecb74fb

add a define context phase

Browse files
src/agents/auditor/agent.ts CHANGED
@@ -1,35 +1,121 @@
 
 
 
1
  import { HumanMessage, SystemMessage } from "@langchain/core/messages";
2
  import { END, type GraphNode, START, StateGraph } from "@langchain/langgraph";
3
  import { z } from "zod";
4
 
5
  import { auditorModel } from "./model.ts";
6
- import {
7
- CRITIC_FINDINGS_PROMPT,
8
- FIND_VULNERABILITIES_PROMPT,
9
- GATHER_CONTEXT_PROMPT,
10
- } from "./prompts.ts";
11
  import { AuditorState, CriticSchema, FindingSchema } from "./state.ts";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
- const MAX_REFLECTIONS = 3;
 
 
 
 
 
 
 
14
 
15
  const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
16
- const model = auditorModel.withStructuredOutput(z.object({ context: z.string() }));
 
 
 
 
 
 
17
 
18
- const scopeNote =
19
- state.scope.length > 0 ? `\nAudit scope (focus on these): ${state.scope.join(", ")}` : "";
 
 
 
 
 
 
20
 
21
- const result = await model.invoke([
22
- new SystemMessage(GATHER_CONTEXT_PROMPT),
23
- new HumanMessage(`Analyze this smart contract:${scopeNote}\n\n${state.solidityFile}`),
24
- ]);
 
 
 
 
 
 
 
 
 
 
25
 
26
- return { repoContext: result.context };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  };
28
 
29
  const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
30
- const model = auditorModel.withStructuredOutput(
31
- z.object({ findings: z.array(FindingSchema) }),
32
- );
33
 
34
  let userMessage = `Contract:\n\n${state.solidityFile}\n\nProtocol Context:\n${state.repoContext}`;
35
 
@@ -43,10 +129,7 @@ const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
43
  userMessage += `\n\nCritic feedback from previous iteration (iteration ${state.reflectionCount}):\n${feedback}\n\nRevise your findings accordingly.`;
44
  }
45
 
46
- const result = await model.invoke([
47
- new SystemMessage(FIND_VULNERABILITIES_PROMPT),
48
- new HumanMessage(userMessage),
49
- ]);
50
 
51
  return { candidateFindings: result.findings };
52
  };
@@ -60,9 +143,7 @@ const criticFindings: GraphNode<typeof AuditorState> = async (state) => {
60
  };
61
  }
62
 
63
- const model = auditorModel.withStructuredOutput(
64
- z.object({ reviews: z.array(CriticSchema) }),
65
- );
66
 
67
  const findingsText = state.candidateFindings
68
  .map(
@@ -93,10 +174,12 @@ const criticFindings: GraphNode<typeof AuditorState> = async (state) => {
93
  };
94
 
95
  export const auditorAgent = new StateGraph(AuditorState)
 
96
  .addNode("gatherContext", gatherContext)
97
  .addNode("findVulnerabilities", findVulnerabilities)
98
  .addNode("criticFindings", criticFindings)
99
- .addEdge(START, "gatherContext")
 
100
  .addEdge("gatherContext", "findVulnerabilities")
101
  .addEdge("findVulnerabilities", "criticFindings")
102
  .addConditionalEdges("criticFindings", (state) => {
 
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 { auditorModel } from "./model.ts";
9
+ import { CRITIC_FINDINGS_PROMPT, FIND_VULNERABILITIES_PROMPT, GATHER_CONTEXT_PROMPT } from "./prompts.ts";
 
 
 
 
10
  import { AuditorState, CriticSchema, FindingSchema } from "./state.ts";
11
+ import { analyzeSolidityFile } from "./tools/solidity-analyzer-tool.ts";
12
+ import {
13
+ DOC_BASENAMES,
14
+ DOC_EXTS,
15
+ MAX_DEPTH,
16
+ MAX_DOC_CHARS,
17
+ MAX_REFLECTIONS,
18
+ MAX_SOL_CHARS,
19
+ SKIP_DIRS,
20
+ SOL_EXT,
21
+ } from "./config.ts";
22
+
23
+ const walkDirectory = (dir: string, depth: number, solFiles: string[], docFiles: string[]) => {
24
+ if (depth > MAX_DEPTH) return;
25
+
26
+ let entries: fs.Dirent[];
27
+ try {
28
+ entries = fs.readdirSync(dir, { withFileTypes: true });
29
+ } catch {
30
+ return;
31
+ }
32
+
33
+ for (const entry of entries) {
34
+ if (entry.isDirectory()) {
35
+ if (!SKIP_DIRS.has(entry.name)) {
36
+ walkDirectory(path.join(dir, entry.name), depth + 1, solFiles, docFiles);
37
+ }
38
+ } else if (entry.isFile()) {
39
+ const fullPath = path.join(dir, entry.name);
40
+ const ext = path.extname(entry.name).toLowerCase();
41
+ const base = path.basename(entry.name, ext).toLowerCase();
42
+
43
+ if (ext === SOL_EXT) {
44
+ solFiles.push(fullPath);
45
+ } else if (DOC_EXTS.has(ext) || DOC_BASENAMES.has(base)) {
46
+ docFiles.push(fullPath);
47
+ }
48
+ }
49
+ }
50
+ };
51
 
52
+ const defineScope: GraphNode<typeof AuditorState> = async (state) => {
53
+ const solFiles: string[] = [];
54
+ const docFiles: string[] = [];
55
+
56
+ walkDirectory(state.repoPath, 0, solFiles, docFiles);
57
+
58
+ return { scope: solFiles, docs: docFiles };
59
+ };
60
 
61
  const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
62
+ const readFile = (filePath: string): string => {
63
+ try {
64
+ return fs.readFileSync(filePath, "utf-8");
65
+ } catch {
66
+ return "";
67
+ }
68
+ };
69
 
70
+ // Read and analyze each Solidity file
71
+ const solidityEntries: { filePath: string; source: string; analysis: string }[] = [];
72
+ for (const filePath of state.scope) {
73
+ const source = readFile(filePath).slice(0, MAX_SOL_CHARS);
74
+ if (!source) continue;
75
+ const analysis = await analyzeSolidityFile(source, "short");
76
+ solidityEntries.push({ filePath, source, analysis });
77
+ }
78
 
79
+ // Concatenate all sources for downstream vulnerability phases
80
+ const solidityFile = solidityEntries
81
+ .map(({ filePath, source }) => `// === FILE: ${filePath} ===\n${source}`)
82
+ .join("\n\n");
83
+
84
+ // Read documentation files
85
+ const docEntries: { filePath: string; content: string }[] = [];
86
+ for (const filePath of state.docs) {
87
+ const content = readFile(filePath).slice(0, MAX_DOC_CHARS);
88
+ if (content) docEntries.push({ filePath, content });
89
+ }
90
+
91
+ // Build the LLM input
92
+ const parts: string[] = [];
93
 
94
+ if (docEntries.length > 0) {
95
+ parts.push("## Documentation\n");
96
+ for (const { filePath, content } of docEntries) {
97
+ parts.push(`### ${filePath}\n${content}`);
98
+ }
99
+ }
100
+
101
+ parts.push("## Structural Analysis (auto-generated)\n");
102
+ for (const { filePath, analysis } of solidityEntries) {
103
+ parts.push(`### ${filePath}\n${analysis}`);
104
+ }
105
+
106
+ parts.push("## Contract Source Code\n");
107
+ for (const { filePath, source } of solidityEntries) {
108
+ parts.push(`### ${filePath}\n\`\`\`solidity\n${source}\n\`\`\``);
109
+ }
110
+
111
+ const model = auditorModel.withStructuredOutput(z.object({ context: z.string() }));
112
+ const result = await model.invoke([new SystemMessage(GATHER_CONTEXT_PROMPT), new HumanMessage(parts.join("\n\n"))]);
113
+
114
+ return { solidityFile, repoContext: result.context };
115
  };
116
 
117
  const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
118
+ const model = auditorModel.withStructuredOutput(z.object({ findings: z.array(FindingSchema) }));
 
 
119
 
120
  let userMessage = `Contract:\n\n${state.solidityFile}\n\nProtocol Context:\n${state.repoContext}`;
121
 
 
129
  userMessage += `\n\nCritic feedback from previous iteration (iteration ${state.reflectionCount}):\n${feedback}\n\nRevise your findings accordingly.`;
130
  }
131
 
132
+ const result = await model.invoke([new SystemMessage(FIND_VULNERABILITIES_PROMPT), new HumanMessage(userMessage)]);
 
 
 
133
 
134
  return { candidateFindings: result.findings };
135
  };
 
143
  };
144
  }
145
 
146
+ const model = auditorModel.withStructuredOutput(z.object({ reviews: z.array(CriticSchema) }));
 
 
147
 
148
  const findingsText = state.candidateFindings
149
  .map(
 
174
  };
175
 
176
  export const auditorAgent = new StateGraph(AuditorState)
177
+ .addNode("defineScope", defineScope)
178
  .addNode("gatherContext", gatherContext)
179
  .addNode("findVulnerabilities", findVulnerabilities)
180
  .addNode("criticFindings", criticFindings)
181
+ .addEdge(START, "defineScope")
182
+ .addEdge("defineScope", "gatherContext")
183
  .addEdge("gatherContext", "findVulnerabilities")
184
  .addEdge("findVulnerabilities", "criticFindings")
185
  .addConditionalEdges("criticFindings", (state) => {
src/agents/auditor/config.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const SOL_EXT = ".sol";
2
+ export const DOC_EXTS = new Set([".md", ".txt", ".rst", ".adoc"]);
3
+ export const DOC_BASENAMES = new Set(["readme", "whitepaper", "spec", "architecture", "design", "overview", "docs"]);
4
+ export const SKIP_DIRS = new Set([
5
+ "node_modules",
6
+ ".git",
7
+ "out",
8
+ "artifacts",
9
+ "cache",
10
+ "lib",
11
+ ".deps",
12
+ "build",
13
+ "dist",
14
+ ]);
15
+ export const MAX_DEPTH = 6;
16
+ export const MAX_DOC_CHARS = 12_000;
17
+ export const MAX_SOL_CHARS = 40_000;
18
+ export const MAX_REFLECTIONS = 3;
src/agents/auditor/prompts.ts CHANGED
@@ -1,14 +1,24 @@
1
- export const GATHER_CONTEXT_PROMPT = `You are a smart contract security expert. Analyze the provided Solidity source code and produce a thorough protocol context that will guide vulnerability discovery.
2
 
3
- Extract and clearly structure the following:
4
 
5
- 1. **Invariants**: Conditions that must always hold (e.g., "total supply must equal sum of all balances", "contract ETH balance >= sum of all user deposits").
 
6
 
7
- 2. **Design Assumptions**: What the protocol assumes about callers, external contracts, oracles, admin keys, and token behavior (e.g., "tokens are ERC-20 compliant", "admin is trusted", "no fee-on-transfer tokens").
 
8
 
9
- 3. **Key Flows**: The main execution paths and state transitions (e.g., deposit β†’ mint shares β†’ updateRewards; withdraw β†’ burn shares β†’ transfer ETH).
 
10
 
11
- 4. **Business Rules**: Access controls, fee structures, timelocks, caps, pausing mechanisms, and any other domain constraints.
 
 
 
 
 
 
 
12
 
13
  Be precise and exhaustive β€” the richer the context, the more accurately vulnerabilities can be identified and validated.`;
14
 
 
1
+ export const GATHER_CONTEXT_PROMPT = `You are a smart contract security expert. You will receive documentation, a structural analysis, and the full source of all in-scope Solidity contracts. Produce a thorough protocol context that will guide vulnerability discovery.
2
 
3
+ Structure your output in the following sections:
4
 
5
+ ## 1. Contract Overview
6
+ For each contract: its purpose, kind (contract/interface/library/abstract), inheritance chain, and key dependencies on other in-scope contracts or external protocols.
7
 
8
+ ## 2. State & Storage Map
9
+ List all meaningful state variables across contracts, what they represent, and which functions read or write them. Flag shared or inherited storage.
10
 
11
+ ## 3. Key Flows
12
+ Trace the main execution paths and state transitions end-to-end across contracts (e.g., deposit β†’ mint shares β†’ updateRewards; withdraw β†’ burn shares β†’ transfer ETH). Include cross-contract calls.
13
 
14
+ ## 4. Invariants
15
+ Conditions that must always hold (e.g., "total supply must equal sum of all balances", "contract ETH balance β‰₯ sum of all user deposits"). Derive these from both the source and any documentation.
16
+
17
+ ## 5. Design Assumptions
18
+ What the protocol assumes about callers, external contracts, oracles, admin keys, and token behavior (e.g., "tokens are ERC-20 compliant", "admin is trusted", "no fee-on-transfer tokens").
19
+
20
+ ## 6. Business Rules
21
+ Access controls, fee structures, timelocks, caps, pausing mechanisms, upgrade patterns, and any other domain constraints.
22
 
23
  Be precise and exhaustive β€” the richer the context, the more accurately vulnerabilities can be identified and validated.`;
24
 
src/agents/auditor/state.ts CHANGED
@@ -22,8 +22,10 @@ export const CriticSchema = z.object({
22
  });
23
 
24
  export const AuditorState = new StateSchema({
 
25
  solidityFile: z.string().default(""),
26
  scope: z.array(z.string()).default([]),
 
27
  repoContext: z.string().default(""),
28
  candidateFindings: z.array(FindingSchema).default([]),
29
  criticReviews: z.array(CriticSchema).default([]),
 
22
  });
23
 
24
  export const AuditorState = new StateSchema({
25
+ repoPath: z.string().default(""),
26
  solidityFile: z.string().default(""),
27
  scope: z.array(z.string()).default([]),
28
+ docs: z.array(z.string()).default([]),
29
  repoContext: z.string().default(""),
30
  candidateFindings: z.array(FindingSchema).default([]),
31
  criticReviews: z.array(CriticSchema).default([]),
src/agents/auditor/tools/solidity-analyzer-tool.ts CHANGED
@@ -434,7 +434,7 @@ const generateMarkdown = (imports: string[], contracts: ContractAnalysis[]) => {
434
  return lines.join("\n");
435
  };
436
 
437
- const analyzeSolidityFile = async (soliditySource: string, mode: "full" | "short") => {
438
  let ast: any;
439
 
440
  try {
 
434
  return lines.join("\n");
435
  };
436
 
437
+ export const analyzeSolidityFile = async (soliditySource: string, mode: "full" | "short") => {
438
  let ast: any;
439
 
440
  try {