Spaces:
Runtime error
Runtime error
Uanderson Silva commited on
Commit ·
881b356
1
Parent(s): 52c3560
adjust audit schemas
Browse files- src/agents/auditor/agent.ts +83 -52
- src/agents/auditor/config.ts +1 -1
- src/agents/auditor/model.ts +2 -2
- src/agents/auditor/state.ts +13 -11
- src/index.ts +29 -16
src/agents/auditor/agent.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { z } from "zod";
|
|
| 8 |
import { logger } from "../../logger.ts";
|
| 9 |
import { judgeFindingsModel, findVulnerabilitiesModel, gatherContextModel } from "./model.ts";
|
| 10 |
import { JUDGE_FINDINGS_PROMPT, FIND_VULNERABILITIES_PROMPT, GATHER_CONTEXT_PROMPT } from "./prompts.ts";
|
| 11 |
-
import { AuditorState,
|
| 12 |
import { analyzeSolidityFile } from "./tools/solidity-analyzer-tool.ts";
|
| 13 |
import { buildRepoTree } from "./tools/repo-tree-tool.ts";
|
| 14 |
import {
|
|
@@ -91,11 +91,6 @@ const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 91 |
solidityEntries.push({ filePath, source, analysis });
|
| 92 |
}
|
| 93 |
|
| 94 |
-
// Concatenate all sources for downstream vulnerability phases
|
| 95 |
-
const solidityFile = solidityEntries
|
| 96 |
-
.map(({ filePath, source }) => `// === FILE: ${filePath} ===\n${source}`)
|
| 97 |
-
.join("\n\n");
|
| 98 |
-
|
| 99 |
// Read documentation files
|
| 100 |
const docEntries: { filePath: string; content: string }[] = [];
|
| 101 |
for (const filePath of state.docs) {
|
|
@@ -123,38 +118,61 @@ const gatherContext: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 123 |
parts.push(`### ${filePath}\n\`\`\`solidity\n${source}\n\`\`\``);
|
| 124 |
}
|
| 125 |
|
| 126 |
-
|
| 127 |
-
|
| 128 |
|
| 129 |
logger.info(`gatherContext: context built (${parts.join("\n\n").length} chars)`);
|
| 130 |
logger.debug(`gatherContext: full context:\n${parts.join("\n\n")}`);
|
| 131 |
|
| 132 |
-
return {
|
| 133 |
};
|
| 134 |
|
| 135 |
const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
|
| 136 |
-
const model = findVulnerabilitiesModel.withStructuredOutput(z.object({ findings: z.array(
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
| 151 |
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
|
| 154 |
-
|
| 155 |
-
logger.
|
|
|
|
| 156 |
|
| 157 |
-
return { candidateFindings
|
| 158 |
};
|
| 159 |
|
| 160 |
const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
|
|
@@ -167,39 +185,52 @@ const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
|
|
| 167 |
};
|
| 168 |
}
|
| 169 |
|
| 170 |
-
const model =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
-
|
| 173 |
-
.map(
|
| 174 |
-
(f, i) =>
|
| 175 |
-
`[Finding ${i + 1}] ${f.title}\nSeverity: ${f.severity}\nDescription: ${f.description}\nLocation: ${f.path} lines ${f.location}\nCode:\n\`\`\`solidity\n${f.codeSnippet}\n\`\`\``,
|
| 176 |
-
)
|
| 177 |
-
.join("\n\n---\n\n");
|
| 178 |
|
| 179 |
-
|
| 180 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
-
const
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
`Contract:\n\n${state.solidityFile}\n\nProtocol Context:\n${state.repoContext}\n\nCandidate Findings to Review:\n\n${findingsText}`,
|
| 186 |
-
),
|
| 187 |
-
]);
|
| 188 |
|
| 189 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
-
const
|
| 192 |
-
const review = reviewsByTitle.get(f.title.toLowerCase()) ?? result.reviews[i];
|
| 193 |
-
return review ? !review.isFalsePositive : true;
|
| 194 |
-
});
|
| 195 |
|
| 196 |
-
|
| 197 |
-
logger.
|
| 198 |
-
logger.debug(`judgeFindings: reviews:\n${JSON.stringify(result.reviews, null, 2)}`);
|
| 199 |
|
| 200 |
return {
|
| 201 |
-
judgeReviews:
|
| 202 |
-
findings
|
| 203 |
reflectionCount: state.reflectionCount + 1,
|
| 204 |
};
|
| 205 |
};
|
|
|
|
| 8 |
import { logger } from "../../logger.ts";
|
| 9 |
import { judgeFindingsModel, findVulnerabilitiesModel, gatherContextModel } from "./model.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 {
|
|
|
|
| 91 |
solidityEntries.push({ filePath, source, analysis });
|
| 92 |
}
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
// Read documentation files
|
| 95 |
const docEntries: { filePath: string; content: string }[] = [];
|
| 96 |
for (const filePath of state.docs) {
|
|
|
|
| 118 |
parts.push(`### ${filePath}\n\`\`\`solidity\n${source}\n\`\`\``);
|
| 119 |
}
|
| 120 |
|
| 121 |
+
const model = gatherContextModel.withStructuredOutput(z.object({ context: z.string() }));
|
| 122 |
+
const result = await model.invoke([new SystemMessage(GATHER_CONTEXT_PROMPT), new HumanMessage(parts.join("\n\n"))]);
|
| 123 |
|
| 124 |
logger.info(`gatherContext: context built (${parts.join("\n\n").length} chars)`);
|
| 125 |
logger.debug(`gatherContext: full context:\n${parts.join("\n\n")}`);
|
| 126 |
|
| 127 |
+
return { repoContext: result.context };
|
| 128 |
};
|
| 129 |
|
| 130 |
const findVulnerabilities: GraphNode<typeof AuditorState> = async (state) => {
|
| 131 |
+
const model = findVulnerabilitiesModel.withStructuredOutput(z.object({ findings: z.array(CandidateFindingSchema) }));
|
| 132 |
+
|
| 133 |
+
const previousFeedback =
|
| 134 |
+
state.judgeReviews.length > 0
|
| 135 |
+
? state.judgeReviews
|
| 136 |
+
.map((r, i) => {
|
| 137 |
+
const title = state.candidateFindings[i]?.title ?? `Finding ${i + 1}`;
|
| 138 |
+
return `- "${title}": ${r.isFalsePositive ? "FALSE POSITIVE" : "TRUE POSITIVE"}\n Judge: ${r.review}`;
|
| 139 |
+
})
|
| 140 |
+
.join("\n")
|
| 141 |
+
: null;
|
| 142 |
+
|
| 143 |
+
logger.info(
|
| 144 |
+
`findVulnerabilities: invoking LLM for ${state.scope.length} file(s) in parallel (iteration ${state.reflectionCount + 1})`,
|
| 145 |
+
);
|
| 146 |
+
|
| 147 |
+
const allFindings = await Promise.all(
|
| 148 |
+
state.scope.map(async (filePath) => {
|
| 149 |
+
let source: string;
|
| 150 |
+
try {
|
| 151 |
+
source = fs.readFileSync(filePath, "utf-8").slice(0, MAX_SOL_CHARS);
|
| 152 |
+
} catch {
|
| 153 |
+
return [];
|
| 154 |
+
}
|
| 155 |
+
if (!source) return [];
|
| 156 |
|
| 157 |
+
let userMessage = `Contract (${filePath}):\n\n${source}\n\nProtocol Context:\n${state.repoContext}`;
|
| 158 |
+
if (previousFeedback) {
|
| 159 |
+
userMessage += `\n\nJudge feedback from previous iteration (iteration ${state.reflectionCount}):\n${previousFeedback}\n\nRevise your findings accordingly.`;
|
| 160 |
+
}
|
| 161 |
|
| 162 |
+
logger.debug(`findVulnerabilities: processing ${filePath}`);
|
| 163 |
+
const result = await model.invoke([
|
| 164 |
+
new SystemMessage(FIND_VULNERABILITIES_PROMPT),
|
| 165 |
+
new HumanMessage(userMessage),
|
| 166 |
+
]);
|
| 167 |
+
return result.findings.map((finding: any) => ({ ...finding, path: filePath, location: "1-14" }));
|
| 168 |
+
}),
|
| 169 |
+
);
|
| 170 |
|
| 171 |
+
const candidateFindings = allFindings.flat();
|
| 172 |
+
logger.info(`findVulnerabilities: LLM returned ${candidateFindings.length} total candidate finding(s)`);
|
| 173 |
+
logger.debug(`findVulnerabilities: findings:\n${JSON.stringify(candidateFindings, null, 2)}`);
|
| 174 |
|
| 175 |
+
return { candidateFindings };
|
| 176 |
};
|
| 177 |
|
| 178 |
const judgeFindings: GraphNode<typeof AuditorState> = async (state) => {
|
|
|
|
| 185 |
};
|
| 186 |
}
|
| 187 |
|
| 188 |
+
const model = judgeFindingsModel.withStructuredOutput(JudgeReviewSchema);
|
| 189 |
+
|
| 190 |
+
logger.info(`judgeFindings: reviewing ${state.candidateFindings.length} candidate finding(s) in parallel`);
|
| 191 |
+
|
| 192 |
+
const reviews = await Promise.all(
|
| 193 |
+
state.candidateFindings.map(async (finding, i) => {
|
| 194 |
+
let source: string;
|
| 195 |
+
try {
|
| 196 |
+
source = fs.readFileSync(finding.path, "utf-8").slice(0, MAX_SOL_CHARS);
|
| 197 |
+
} catch {
|
| 198 |
+
source = "";
|
| 199 |
+
}
|
| 200 |
|
| 201 |
+
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\`\`\``;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
|
| 203 |
+
logger.debug(`judgeFindings: reviewing finding ${i + 1}: ${finding.title}`);
|
| 204 |
+
return model.invoke([
|
| 205 |
+
new SystemMessage(JUDGE_FINDINGS_PROMPT),
|
| 206 |
+
new HumanMessage(
|
| 207 |
+
`Contract (${finding.path}):\n\n${source}\n\nProtocol Context:\n${state.repoContext}\n\nFinding to Review:\n\n${findingText}`,
|
| 208 |
+
),
|
| 209 |
+
]);
|
| 210 |
+
}),
|
| 211 |
+
);
|
| 212 |
|
| 213 |
+
const confirmedEntries = state.candidateFindings
|
| 214 |
+
.map((finding, i) => ({ finding, review: reviews[i] }))
|
| 215 |
+
.filter(({ review }) => !review.isFalsePositive);
|
|
|
|
|
|
|
|
|
|
| 216 |
|
| 217 |
+
const findings = confirmedEntries.map(({ finding, review }) => ({
|
| 218 |
+
...finding,
|
| 219 |
+
judgeReview: {
|
| 220 |
+
review: review.review,
|
| 221 |
+
confidence: review.confidence,
|
| 222 |
+
exploitablePaths: review.exploitablePaths,
|
| 223 |
+
},
|
| 224 |
+
}));
|
| 225 |
|
| 226 |
+
const falsePositiveCount = state.candidateFindings.length - findings.length;
|
|
|
|
|
|
|
|
|
|
| 227 |
|
| 228 |
+
logger.info(`judgeFindings: ${findings.length} confirmed, ${falsePositiveCount} false positive(s)`);
|
| 229 |
+
logger.debug(`judgeFindings: reviews:\n${JSON.stringify(reviews, null, 2)}`);
|
|
|
|
| 230 |
|
| 231 |
return {
|
| 232 |
+
judgeReviews: reviews,
|
| 233 |
+
findings,
|
| 234 |
reflectionCount: state.reflectionCount + 1,
|
| 235 |
};
|
| 236 |
};
|
src/agents/auditor/config.ts
CHANGED
|
@@ -20,4 +20,4 @@ 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 =
|
|
|
|
| 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;
|
src/agents/auditor/model.ts
CHANGED
|
@@ -5,9 +5,9 @@ export const gatherContextModel = new ChatAnthropic({
|
|
| 5 |
});
|
| 6 |
|
| 7 |
export const findVulnerabilitiesModel = new ChatAnthropic({
|
| 8 |
-
model: "claude-
|
| 9 |
});
|
| 10 |
|
| 11 |
export const judgeFindingsModel = new ChatAnthropic({
|
| 12 |
-
model: "claude-
|
| 13 |
});
|
|
|
|
| 5 |
});
|
| 6 |
|
| 7 |
export const findVulnerabilitiesModel = new ChatAnthropic({
|
| 8 |
+
model: "claude-haiku-4-5",
|
| 9 |
});
|
| 10 |
|
| 11 |
export const judgeFindingsModel = new ChatAnthropic({
|
| 12 |
+
model: "claude-haiku-4-5",
|
| 13 |
});
|
src/agents/auditor/state.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import { StateSchema } from "@langchain/langgraph";
|
| 2 |
import { z } from "zod";
|
| 3 |
|
| 4 |
-
export const
|
| 5 |
title: z.string(),
|
| 6 |
description: z.string(),
|
| 7 |
recommendation: z.string(),
|
|
@@ -9,32 +9,34 @@ export const PartialFindingSchema = z.object({
|
|
| 9 |
codeSnippet: z.string(),
|
| 10 |
});
|
| 11 |
|
| 12 |
-
export const
|
| 13 |
-
title: z.string(),
|
| 14 |
-
description: z.string(),
|
| 15 |
-
recommendation: z.string(),
|
| 16 |
-
severity: z.enum(["high", "medium", "low"]),
|
| 17 |
-
codeSnippet: z.string(),
|
| 18 |
location: z.string(),
|
| 19 |
path: z.string(),
|
| 20 |
});
|
| 21 |
|
| 22 |
-
export const
|
| 23 |
review: z.string(),
|
| 24 |
isFalsePositive: z.boolean(),
|
| 25 |
confidence: z.number(),
|
| 26 |
exploitablePaths: z.array(z.string()),
|
| 27 |
});
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
export const AuditorState = new StateSchema({
|
| 30 |
repoPath: z.string().default(""),
|
| 31 |
-
solidityFile: z.string().default(""),
|
| 32 |
scope: z.array(z.string()).default([]),
|
| 33 |
docs: z.array(z.string()).default([]),
|
| 34 |
fileTree: z.string().default(""),
|
| 35 |
repoContext: z.string().default(""),
|
| 36 |
-
candidateFindings: z.array(
|
| 37 |
-
judgeReviews: z.array(
|
| 38 |
findings: z.array(FindingSchema).default([]),
|
| 39 |
reflectionCount: z.number().default(0),
|
| 40 |
});
|
|
|
|
| 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(),
|
| 7 |
recommendation: z.string(),
|
|
|
|
| 9 |
codeSnippet: z.string(),
|
| 10 |
});
|
| 11 |
|
| 12 |
+
export const LocatedFindingSchema = CandidateFindingSchema.extend({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
location: z.string(),
|
| 14 |
path: z.string(),
|
| 15 |
});
|
| 16 |
|
| 17 |
+
export const JudgeReviewSchema = z.object({
|
| 18 |
review: z.string(),
|
| 19 |
isFalsePositive: z.boolean(),
|
| 20 |
confidence: z.number(),
|
| 21 |
exploitablePaths: z.array(z.string()),
|
| 22 |
});
|
| 23 |
|
| 24 |
+
export const FindingSchema = LocatedFindingSchema.extend({
|
| 25 |
+
judgeReview: z.object({
|
| 26 |
+
review: z.string(),
|
| 27 |
+
confidence: z.number(),
|
| 28 |
+
exploitablePaths: z.array(z.string()),
|
| 29 |
+
}),
|
| 30 |
+
});
|
| 31 |
+
|
| 32 |
export const AuditorState = new StateSchema({
|
| 33 |
repoPath: z.string().default(""),
|
|
|
|
| 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([]),
|
| 40 |
findings: z.array(FindingSchema).default([]),
|
| 41 |
reflectionCount: z.number().default(0),
|
| 42 |
});
|
src/index.ts
CHANGED
|
@@ -1,22 +1,35 @@
|
|
| 1 |
-
import "dotenv/config";
|
| 2 |
|
| 3 |
-
import { auditorAgent } from "./agents/auditor/agent.ts";
|
| 4 |
-
import { coderAgent } from "./agents/coder/agent.ts";
|
| 5 |
-
import { testerAgent } from "./agents/tester/agent.ts";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
-
const
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
const
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
| 20 |
});
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
| 1 |
+
// import "dotenv/config";
|
| 2 |
|
| 3 |
+
// import { auditorAgent } from "./agents/auditor/agent.ts";
|
| 4 |
+
// import { coderAgent } from "./agents/coder/agent.ts";
|
| 5 |
+
// import { testerAgent } from "./agents/tester/agent.ts";
|
| 6 |
+
|
| 7 |
+
// const requirements = ["ERC20 token", "pausable", "ownable"];
|
| 8 |
+
|
| 9 |
+
// const coderResult = await coderAgent.invoke({ requirements });
|
| 10 |
+
// console.log("======= Coder =======");
|
| 11 |
+
// console.log(coderResult.contract);
|
| 12 |
|
| 13 |
+
// const auditorResult = await auditorAgent.invoke({ solidityFile: coderResult.contract });
|
| 14 |
+
// console.log("\n======= Auditor =======");
|
| 15 |
+
// console.log(auditorResult.findings);
|
| 16 |
|
| 17 |
+
// const testerResult = await testerAgent.invoke({
|
| 18 |
+
// solidityFiles: [coderResult.contract],
|
| 19 |
+
// vulnerability: auditorResult.findings[0] ?? {},
|
| 20 |
+
// });
|
| 21 |
+
// console.log("\n======= Tester =======");
|
| 22 |
+
// console.log(testerResult.results);
|
| 23 |
|
| 24 |
+
import "dotenv/config";
|
| 25 |
+
import { auditorAgent } from "./agents/auditor/agent.ts";
|
| 26 |
+
import { logger } from "./logger.ts";
|
| 27 |
|
| 28 |
+
logger.info("Starting auditorAgent");
|
| 29 |
+
|
| 30 |
+
const result = await auditorAgent.invoke({
|
| 31 |
+
repoPath: "/Users/uanderson/personal/projeto-talp1/dist/repos/example",
|
| 32 |
});
|
| 33 |
+
|
| 34 |
+
logger.info("Agent completed");
|
| 35 |
+
logger.debug(`Agent result:\n${JSON.stringify(result, null, 2)}`);
|