Spaces:
Runtime error
Runtime error
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
|
| 16 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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, "
|
|
|
|
| 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.
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
1.
|
|
|
|
| 6 |
|
| 7 |
-
2.
|
|
|
|
| 8 |
|
| 9 |
-
3.
|
|
|
|
| 10 |
|
| 11 |
-
4.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 {
|