solidity-argus 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +37 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/package.json +43 -0
- package/skills/INVENTORY.md +79 -0
- package/skills/README.md +56 -0
- package/skills/checklists/cyfrin-best-practices-runtime/SKILL.md +424 -0
- package/skills/checklists/cyfrin-best-practices-upgrades/SKILL.md +157 -0
- package/skills/checklists/cyfrin-defi-core/SKILL.md +373 -0
- package/skills/checklists/cyfrin-defi-integrations/SKILL.md +412 -0
- package/skills/checklists/cyfrin-gas/SKILL.md +55 -0
- package/skills/checklists/general-audit/SKILL.md +433 -0
- package/skills/methodology/audit-workflow/SKILL.md +129 -0
- package/skills/methodology/report-template/SKILL.md +190 -0
- package/skills/methodology/severity-classification/SKILL.md +179 -0
- package/skills/protocol-patterns/amm-dex/SKILL.md +229 -0
- package/skills/protocol-patterns/bridges-cross-chain/SKILL.md +317 -0
- package/skills/protocol-patterns/dao-governance/SKILL.md +281 -0
- package/skills/protocol-patterns/lending-borrowing/SKILL.md +221 -0
- package/skills/protocol-patterns/staking-vesting/SKILL.md +247 -0
- package/skills/references/exploit-reference/SKILL.md +259 -0
- package/skills/references/smartbugs-examples/SKILL.md +296 -0
- package/skills/vulnerability-patterns/access-control/SKILL.md +298 -0
- package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +59 -0
- package/skills/vulnerability-patterns/assert-violation/SKILL.md +59 -0
- package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +61 -0
- package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +55 -0
- package/skills/vulnerability-patterns/default-visibility/SKILL.md +62 -0
- package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +60 -0
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +59 -0
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +72 -0
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +249 -0
- package/skills/vulnerability-patterns/floating-pragma/SKILL.md +51 -0
- package/skills/vulnerability-patterns/hash-collision/SKILL.md +52 -0
- package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +61 -0
- package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +60 -0
- package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +59 -0
- package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +61 -0
- package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +61 -0
- package/skills/vulnerability-patterns/logic-errors/SKILL.md +333 -0
- package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +60 -0
- package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +66 -0
- package/skills/vulnerability-patterns/off-by-one/SKILL.md +67 -0
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +252 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +65 -0
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +61 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +266 -0
- package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +72 -0
- package/skills/vulnerability-patterns/signature-malleability/SKILL.md +59 -0
- package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +63 -0
- package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +52 -0
- package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +65 -0
- package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +61 -0
- package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +63 -0
- package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +56 -0
- package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +80 -0
- package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +69 -0
- package/skills/vulnerability-patterns/unused-variables/SKILL.md +70 -0
- package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +81 -0
- package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +77 -0
- package/skills/vulnerability-patterns/weird-tokens/SKILL.md +294 -0
- package/src/agents/argus-prompt.ts +407 -0
- package/src/agents/pythia-prompt.ts +134 -0
- package/src/agents/scribe-prompt.ts +87 -0
- package/src/agents/sentinel-prompt.ts +133 -0
- package/src/cli/cli-program.ts +67 -0
- package/src/cli/commands/doctor.ts +83 -0
- package/src/cli/commands/init.ts +46 -0
- package/src/cli/commands/install.ts +55 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/tui-prompts.ts +75 -0
- package/src/cli/types.ts +9 -0
- package/src/config/index.ts +3 -0
- package/src/config/loader.ts +36 -0
- package/src/config/schema.ts +82 -0
- package/src/config/types.ts +4 -0
- package/src/constants/defaults.ts +6 -0
- package/src/create-hooks.ts +84 -0
- package/src/create-managers.ts +26 -0
- package/src/create-tools.ts +30 -0
- package/src/features/audit-enforcer/audit-enforcer.ts +34 -0
- package/src/features/audit-enforcer/index.ts +1 -0
- package/src/features/background-agent/background-manager.ts +200 -0
- package/src/features/background-agent/index.ts +1 -0
- package/src/features/context-monitor/context-monitor.ts +48 -0
- package/src/features/context-monitor/index.ts +4 -0
- package/src/features/context-monitor/tool-output-truncator.ts +17 -0
- package/src/features/error-recovery/index.ts +2 -0
- package/src/features/error-recovery/session-recovery.ts +27 -0
- package/src/features/error-recovery/tool-error-recovery.ts +35 -0
- package/src/features/index.ts +5 -0
- package/src/features/persistent-state/audit-state-manager.ts +121 -0
- package/src/features/persistent-state/index.ts +1 -0
- package/src/hooks/compaction-hook.ts +50 -0
- package/src/hooks/config-handler.ts +116 -0
- package/src/hooks/event-hook-v2.ts +93 -0
- package/src/hooks/event-hook.ts +74 -0
- package/src/hooks/hook-system.ts +9 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/knowledge-sync-hook.ts +57 -0
- package/src/hooks/safe-create-hook.ts +15 -0
- package/src/hooks/system-prompt-hook.ts +126 -0
- package/src/hooks/tool-tracking-hook.ts +234 -0
- package/src/hooks/types.ts +16 -0
- package/src/index.ts +36 -0
- package/src/knowledge/scvd-client.ts +242 -0
- package/src/knowledge/scvd-index.ts +183 -0
- package/src/knowledge/scvd-sync.ts +85 -0
- package/src/managers/index.ts +1 -0
- package/src/managers/types.ts +85 -0
- package/src/plugin-interface.ts +38 -0
- package/src/shared/binary-utils.ts +63 -0
- package/src/shared/deep-merge.ts +71 -0
- package/src/shared/file-utils.ts +56 -0
- package/src/shared/index.ts +5 -0
- package/src/shared/jsonc-parser.ts +39 -0
- package/src/shared/logger.ts +36 -0
- package/src/state/audit-state.ts +27 -0
- package/src/state/finding-store.ts +126 -0
- package/src/state/plugin-state.ts +14 -0
- package/src/state/types.ts +61 -0
- package/src/tools/contract-analyzer-tool.ts +184 -0
- package/src/tools/forge-fuzz-tool.ts +311 -0
- package/src/tools/forge-test-tool.ts +397 -0
- package/src/tools/pattern-checker-tool.ts +337 -0
- package/src/tools/report-generator-tool.ts +308 -0
- package/src/tools/slither-tool.ts +465 -0
- package/src/tools/solodit-search-tool.ts +131 -0
- package/src/tools/sync-knowledge-tool.ts +116 -0
- package/src/utils/project-detector.ts +133 -0
- package/src/utils/solidity-parser.ts +174 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { mkdtempSync, writeFileSync, rmSync, readFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { tool, type ToolContext } from "@opencode-ai/plugin";
|
|
7
|
+
import type { Finding, FindingSeverity } from "../state/types";
|
|
8
|
+
import { hasBinary as hasBinaryShared, parseSolcVersion as parseSolcVersionShared, extractContractNames as extractContractNamesShared } from "../shared/binary-utils";
|
|
9
|
+
|
|
10
|
+
type SlitherArgs = {
|
|
11
|
+
target: string;
|
|
12
|
+
detectors?: string[];
|
|
13
|
+
exclude?: string[];
|
|
14
|
+
solc_version?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type SlitherDetector = {
|
|
18
|
+
check?: string;
|
|
19
|
+
impact?: string;
|
|
20
|
+
confidence?: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
elements?: Array<{
|
|
23
|
+
source_mapping?: {
|
|
24
|
+
filename_relative?: string;
|
|
25
|
+
lines?: number[];
|
|
26
|
+
};
|
|
27
|
+
}>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type SlitherPayload = {
|
|
31
|
+
success?: boolean;
|
|
32
|
+
error?: string | null;
|
|
33
|
+
results?: {
|
|
34
|
+
detectors?: SlitherDetector[];
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type SlitherRunResult = {
|
|
39
|
+
stdout: string;
|
|
40
|
+
stderr: string;
|
|
41
|
+
exitCode: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type RunSlitherCommand = (
|
|
45
|
+
command: string[],
|
|
46
|
+
signal: AbortSignal
|
|
47
|
+
) => Promise<SlitherRunResult>;
|
|
48
|
+
|
|
49
|
+
export type SlitherAnalyzeResult = {
|
|
50
|
+
success: boolean;
|
|
51
|
+
findingsCount: number;
|
|
52
|
+
findings: Finding[];
|
|
53
|
+
executionTime: number;
|
|
54
|
+
errors: string[];
|
|
55
|
+
error?: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function mapSeverity(impact?: string): FindingSeverity {
|
|
59
|
+
switch (impact) {
|
|
60
|
+
case "High":
|
|
61
|
+
return "High";
|
|
62
|
+
case "Medium":
|
|
63
|
+
return "Medium";
|
|
64
|
+
case "Low":
|
|
65
|
+
return "Low";
|
|
66
|
+
case "Informational":
|
|
67
|
+
return "Informational";
|
|
68
|
+
default:
|
|
69
|
+
return "Informational";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function mapConfidence(confidence?: string): "High" | "Medium" | "Low" {
|
|
74
|
+
switch (confidence) {
|
|
75
|
+
case "High":
|
|
76
|
+
return "High";
|
|
77
|
+
case "Medium":
|
|
78
|
+
return "Medium";
|
|
79
|
+
case "Low":
|
|
80
|
+
return "Low";
|
|
81
|
+
default:
|
|
82
|
+
return "Low";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function findingLines(lines?: number[]): [number, number] {
|
|
87
|
+
if (!lines || lines.length === 0) {
|
|
88
|
+
return [1, 1];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (lines.length === 1) {
|
|
92
|
+
const only = lines[0] ?? 1;
|
|
93
|
+
return [only, only];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const start = lines[0] ?? 1;
|
|
97
|
+
const end = lines[lines.length - 1] ?? start;
|
|
98
|
+
return [start, end];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function createFindingID(check: string, file: string, lines: [number, number]): string {
|
|
102
|
+
const key = `${check}:${file}:${lines[0]}-${lines[1]}`;
|
|
103
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildCommand(args: SlitherArgs): string[] {
|
|
107
|
+
const command = [
|
|
108
|
+
"slither",
|
|
109
|
+
args.target,
|
|
110
|
+
"--json",
|
|
111
|
+
"-",
|
|
112
|
+
"--filter-paths",
|
|
113
|
+
"node_modules",
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
if (args.detectors && args.detectors.length > 0) {
|
|
117
|
+
command.push("--detect", args.detectors.join(","));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (args.exclude && args.exclude.length > 0) {
|
|
121
|
+
command.push("--exclude-detectors", args.exclude.join(","));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (args.solc_version) {
|
|
125
|
+
command.push("--solc", `solc:${args.solc_version}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return command;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const FALLBACK_TRIGGERS = [
|
|
132
|
+
"Contract",
|
|
133
|
+
"not found",
|
|
134
|
+
"AssertionError",
|
|
135
|
+
"crytic_compile",
|
|
136
|
+
"empty AST",
|
|
137
|
+
"Compilation failed",
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
function shouldTryFlattenFallback(errors: string[], stderr: string): boolean {
|
|
141
|
+
const combined = [...errors, stderr].join(" ");
|
|
142
|
+
return FALLBACK_TRIGGERS.some((trigger) => combined.includes(trigger));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const parseSolcVersion = parseSolcVersionShared
|
|
146
|
+
const extractContractNames = extractContractNamesShared
|
|
147
|
+
const hasBinary = hasBinaryShared
|
|
148
|
+
|
|
149
|
+
function ensureSolc(version: string): boolean {
|
|
150
|
+
if (hasBinary("solc")) return true;
|
|
151
|
+
if (!hasBinary("solc-select")) return false;
|
|
152
|
+
try {
|
|
153
|
+
execSync(`solc-select install ${version} && solc-select use ${version}`, {
|
|
154
|
+
stdio: "ignore",
|
|
155
|
+
timeout: 60_000,
|
|
156
|
+
});
|
|
157
|
+
return true;
|
|
158
|
+
} catch (_e) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const runSlitherCommand: RunSlitherCommand = async (command, signal) => {
|
|
164
|
+
const child = Bun.spawn(command, {
|
|
165
|
+
stdout: "pipe",
|
|
166
|
+
stderr: "pipe",
|
|
167
|
+
signal,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
171
|
+
child.exited,
|
|
172
|
+
new Response(child.stdout).text(),
|
|
173
|
+
new Response(child.stderr).text(),
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
stdout,
|
|
178
|
+
stderr,
|
|
179
|
+
exitCode,
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export type FlattenFallbackDeps = {
|
|
184
|
+
runCommand: RunSlitherCommand;
|
|
185
|
+
hasBinary: (name: string) => boolean;
|
|
186
|
+
ensureSolc: (version: string) => boolean;
|
|
187
|
+
parseSolcVersion: (target: string) => string | undefined;
|
|
188
|
+
extractContractNames: (filePath: string) => string[];
|
|
189
|
+
execSyncFn: typeof execSync;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const defaultFlattenDeps: FlattenFallbackDeps = {
|
|
193
|
+
runCommand: runSlitherCommand,
|
|
194
|
+
hasBinary,
|
|
195
|
+
ensureSolc,
|
|
196
|
+
parseSolcVersion,
|
|
197
|
+
extractContractNames,
|
|
198
|
+
execSyncFn: execSync,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
export async function flattenFallback(
|
|
202
|
+
args: SlitherArgs,
|
|
203
|
+
context: ToolContext,
|
|
204
|
+
deps: FlattenFallbackDeps = defaultFlattenDeps,
|
|
205
|
+
): Promise<SlitherAnalyzeResult | undefined> {
|
|
206
|
+
const startedAt = Date.now();
|
|
207
|
+
|
|
208
|
+
if (!deps.hasBinary("forge")) {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const solcVersion = args.solc_version ?? deps.parseSolcVersion(args.target);
|
|
213
|
+
if (!solcVersion) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!deps.ensureSolc(solcVersion)) {
|
|
218
|
+
return {
|
|
219
|
+
success: false,
|
|
220
|
+
findingsCount: 0,
|
|
221
|
+
findings: [],
|
|
222
|
+
executionTime: Date.now() - startedAt,
|
|
223
|
+
errors: ["solc not available and solc-select not found"],
|
|
224
|
+
error: "Flatten fallback requires solc on PATH. Install with: pipx install solc-select && solc-select install " + solcVersion,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const srcDir = join(args.target, "src");
|
|
229
|
+
let solFiles: string[] = [];
|
|
230
|
+
if (args.target.endsWith(".sol")) {
|
|
231
|
+
solFiles = [args.target];
|
|
232
|
+
} else if (existsSync(srcDir)) {
|
|
233
|
+
try {
|
|
234
|
+
solFiles = deps.execSyncFn(`find "${srcDir}" -name "*.sol" -maxdepth 3 -not -path "*/mocks/*" -not -path "*/test/*"`, {
|
|
235
|
+
encoding: "utf-8",
|
|
236
|
+
timeout: 5_000,
|
|
237
|
+
})
|
|
238
|
+
.trim()
|
|
239
|
+
.split("\n")
|
|
240
|
+
.filter(Boolean);
|
|
241
|
+
} catch (_e) {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (solFiles.length === 0) return undefined;
|
|
247
|
+
|
|
248
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "argus-slither-"));
|
|
249
|
+
const allFindings: Finding[] = [];
|
|
250
|
+
const errors: string[] = [];
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
for (const solFile of solFiles) {
|
|
254
|
+
if (context.abort.aborted) break;
|
|
255
|
+
|
|
256
|
+
const baseName = solFile.split("/").pop()?.replace(".sol", "") ?? "Contract";
|
|
257
|
+
const flatFile = join(tmpDir, `${baseName}.flat.sol`);
|
|
258
|
+
const originalContracts = deps.extractContractNames(solFile);
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const flattened = deps.execSyncFn(`forge flatten "${solFile}"`, {
|
|
262
|
+
encoding: "utf-8",
|
|
263
|
+
timeout: 30_000,
|
|
264
|
+
cwd: args.target.endsWith(".sol") ? undefined : args.target,
|
|
265
|
+
});
|
|
266
|
+
writeFileSync(flatFile, flattened);
|
|
267
|
+
} catch (_e) {
|
|
268
|
+
errors.push(`forge flatten failed for ${solFile}`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const command = [
|
|
273
|
+
"slither",
|
|
274
|
+
flatFile,
|
|
275
|
+
"--json",
|
|
276
|
+
"-",
|
|
277
|
+
"--solc-solcs-select",
|
|
278
|
+
solcVersion,
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const runResult = await deps.runCommand(command, context.abort);
|
|
283
|
+
|
|
284
|
+
let payload: SlitherPayload;
|
|
285
|
+
try {
|
|
286
|
+
payload = JSON.parse(runResult.stdout) as SlitherPayload;
|
|
287
|
+
} catch (_e) {
|
|
288
|
+
if (runResult.stderr.trim()) errors.push(runResult.stderr.trim());
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const rawFindings = parseFindings(payload);
|
|
293
|
+
const filtered = originalContracts.length > 0
|
|
294
|
+
? rawFindings.filter((f) => {
|
|
295
|
+
if (f.file.includes(".flat.sol") || f.file === flatFile) return true;
|
|
296
|
+
return originalContracts.some(
|
|
297
|
+
(name) => f.description.includes(name) || f.file.includes(name)
|
|
298
|
+
);
|
|
299
|
+
})
|
|
300
|
+
: rawFindings;
|
|
301
|
+
|
|
302
|
+
const remapped = filtered.map((f) => ({
|
|
303
|
+
...f,
|
|
304
|
+
file: f.file.includes(".flat.sol") ? solFile.replace(args.target + "/", "") : f.file,
|
|
305
|
+
}));
|
|
306
|
+
|
|
307
|
+
allFindings.push(...remapped);
|
|
308
|
+
} catch (e) {
|
|
309
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
310
|
+
errors.push(`Slither flatten fallback failed for ${baseName}: ${msg}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
success: allFindings.length > 0 || errors.length === 0,
|
|
316
|
+
findingsCount: allFindings.length,
|
|
317
|
+
findings: allFindings,
|
|
318
|
+
executionTime: Date.now() - startedAt,
|
|
319
|
+
errors: errors.length > 0 ? [`[flatten-fallback] ${errors.join("; ")}`] : ["[flatten-fallback] Analysis completed via forge flatten"],
|
|
320
|
+
};
|
|
321
|
+
} finally {
|
|
322
|
+
try {
|
|
323
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
324
|
+
} catch (_cleanupErr) {
|
|
325
|
+
// best-effort: temp dir cleanup failure is non-fatal
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function parseFindings(payload: SlitherPayload): Finding[] {
|
|
331
|
+
const detectors = payload.results?.detectors ?? [];
|
|
332
|
+
|
|
333
|
+
return detectors.map((detector) => {
|
|
334
|
+
const file = detector.elements?.[0]?.source_mapping?.filename_relative ?? "unknown";
|
|
335
|
+
const lines = findingLines(detector.elements?.[0]?.source_mapping?.lines);
|
|
336
|
+
const check = detector.check ?? "unknown-check";
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
id: createFindingID(check, file, lines),
|
|
340
|
+
check,
|
|
341
|
+
severity: mapSeverity(detector.impact),
|
|
342
|
+
confidence: mapConfidence(detector.confidence),
|
|
343
|
+
description: detector.description ?? "",
|
|
344
|
+
file,
|
|
345
|
+
lines,
|
|
346
|
+
source: "slither",
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function executeSlitherAnalyze(
|
|
352
|
+
args: SlitherArgs,
|
|
353
|
+
context: ToolContext,
|
|
354
|
+
runCommand: RunSlitherCommand = runSlitherCommand
|
|
355
|
+
): Promise<SlitherAnalyzeResult> {
|
|
356
|
+
const startedAt = Date.now();
|
|
357
|
+
const command = buildCommand(args);
|
|
358
|
+
context.metadata({ title: `Slither analysis: ${args.target}` });
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
const runResult = await runCommand(command, context.abort);
|
|
362
|
+
const errors: string[] = [];
|
|
363
|
+
|
|
364
|
+
if (runResult.exitCode !== 0) {
|
|
365
|
+
errors.push(`Slither exited with code ${runResult.exitCode}`);
|
|
366
|
+
}
|
|
367
|
+
if (runResult.stderr.trim().length > 0) {
|
|
368
|
+
errors.push(runResult.stderr.trim());
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let payload: SlitherPayload;
|
|
372
|
+
try {
|
|
373
|
+
payload = JSON.parse(runResult.stdout) as SlitherPayload;
|
|
374
|
+
} catch (error) {
|
|
375
|
+
const message = error instanceof Error ? error.message : "Unknown parse error";
|
|
376
|
+
if (shouldTryFlattenFallback(errors, runResult.stderr)) {
|
|
377
|
+
const fallbackResult = await flattenFallback(args, context, {
|
|
378
|
+
...defaultFlattenDeps,
|
|
379
|
+
runCommand,
|
|
380
|
+
});
|
|
381
|
+
if (fallbackResult) return fallbackResult;
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
success: false,
|
|
385
|
+
findingsCount: 0,
|
|
386
|
+
findings: [],
|
|
387
|
+
executionTime: Date.now() - startedAt,
|
|
388
|
+
errors,
|
|
389
|
+
error: `Slither output parse error: ${message}`,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (payload.error) {
|
|
394
|
+
errors.push(payload.error);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const findings = parseFindings(payload);
|
|
398
|
+
const success = findings.length > 0 || (runResult.exitCode === 0 && payload.success !== false);
|
|
399
|
+
|
|
400
|
+
if (!success && findings.length === 0 && shouldTryFlattenFallback(errors, runResult.stderr)) {
|
|
401
|
+
const fallbackResult = await flattenFallback(args, context, {
|
|
402
|
+
...defaultFlattenDeps,
|
|
403
|
+
runCommand,
|
|
404
|
+
});
|
|
405
|
+
if (fallbackResult) return fallbackResult;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
success,
|
|
410
|
+
findingsCount: findings.length,
|
|
411
|
+
findings,
|
|
412
|
+
executionTime: Date.now() - startedAt,
|
|
413
|
+
errors,
|
|
414
|
+
};
|
|
415
|
+
} catch (error) {
|
|
416
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
417
|
+
const maybeErrno = error as Error & { code?: string; name?: string };
|
|
418
|
+
|
|
419
|
+
if (maybeErrno.code === "ENOENT") {
|
|
420
|
+
return {
|
|
421
|
+
success: false,
|
|
422
|
+
findingsCount: 0,
|
|
423
|
+
findings: [],
|
|
424
|
+
executionTime: Date.now() - startedAt,
|
|
425
|
+
errors: [],
|
|
426
|
+
error: "Slither not found. Install with: pip install slither-analyzer",
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (maybeErrno.name === "AbortError" || context.abort.aborted) {
|
|
431
|
+
return {
|
|
432
|
+
success: false,
|
|
433
|
+
findingsCount: 0,
|
|
434
|
+
findings: [],
|
|
435
|
+
executionTime: Date.now() - startedAt,
|
|
436
|
+
errors: ["Slither analysis aborted"],
|
|
437
|
+
error: "Slither analysis aborted",
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
success: false,
|
|
443
|
+
findingsCount: 0,
|
|
444
|
+
findings: [],
|
|
445
|
+
executionTime: Date.now() - startedAt,
|
|
446
|
+
errors: [message],
|
|
447
|
+
error: message,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export const slitherTool = tool({
|
|
453
|
+
description:
|
|
454
|
+
"Run Slither static analysis and return normalized findings for Solidity targets.",
|
|
455
|
+
args: {
|
|
456
|
+
target: tool.schema.string(),
|
|
457
|
+
detectors: tool.schema.array(tool.schema.string()).optional(),
|
|
458
|
+
exclude: tool.schema.array(tool.schema.string()).optional(),
|
|
459
|
+
solc_version: tool.schema.string().optional(),
|
|
460
|
+
},
|
|
461
|
+
async execute(args, context) {
|
|
462
|
+
const result = await executeSlitherAnalyze(args, context);
|
|
463
|
+
return JSON.stringify(result);
|
|
464
|
+
},
|
|
465
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { tool, type ToolContext } from "@opencode-ai/plugin";
|
|
2
|
+
|
|
3
|
+
const SOLODIT_MCP_SERVER = "solodit-mcp";
|
|
4
|
+
const SOLODIT_MCP_TOOL = "search_findings";
|
|
5
|
+
const DEFAULT_LIMIT = 10;
|
|
6
|
+
|
|
7
|
+
type SoloditSearchArgs = {
|
|
8
|
+
query: string;
|
|
9
|
+
severity?: string[];
|
|
10
|
+
limit?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type SoloditFinding = {
|
|
14
|
+
title: string;
|
|
15
|
+
severity: string;
|
|
16
|
+
description: string;
|
|
17
|
+
protocol: string;
|
|
18
|
+
url: string;
|
|
19
|
+
remediation: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type SoloditSearchResult = {
|
|
23
|
+
results: SoloditFinding[];
|
|
24
|
+
totalFound: number;
|
|
25
|
+
query: string;
|
|
26
|
+
error?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type CallMcpTool = (
|
|
30
|
+
server: string,
|
|
31
|
+
tool: string,
|
|
32
|
+
args: Record<string, unknown>
|
|
33
|
+
) => Promise<unknown>;
|
|
34
|
+
|
|
35
|
+
type McpCapableContext = ToolContext & { callMcpTool: CallMcpTool };
|
|
36
|
+
|
|
37
|
+
function hasMcpCapability(ctx: ToolContext): ctx is McpCapableContext {
|
|
38
|
+
return "callMcpTool" in ctx;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseFinding(raw: unknown): SoloditFinding {
|
|
42
|
+
if (typeof raw !== "object" || raw === null) {
|
|
43
|
+
return {
|
|
44
|
+
title: "",
|
|
45
|
+
severity: "",
|
|
46
|
+
description: "",
|
|
47
|
+
protocol: "",
|
|
48
|
+
url: "",
|
|
49
|
+
remediation: "",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const obj = raw as Record<string, unknown>;
|
|
54
|
+
return {
|
|
55
|
+
title: typeof obj["title"] === "string" ? obj["title"] : "",
|
|
56
|
+
severity: typeof obj["severity"] === "string" ? obj["severity"] : "",
|
|
57
|
+
description: typeof obj["description"] === "string" ? obj["description"] : "",
|
|
58
|
+
protocol: typeof obj["protocol"] === "string" ? obj["protocol"] : "",
|
|
59
|
+
url: typeof obj["url"] === "string" ? obj["url"] : "",
|
|
60
|
+
remediation: typeof obj["remediation"] === "string" ? obj["remediation"] : "",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseFindings(response: unknown): SoloditFinding[] {
|
|
65
|
+
if (!Array.isArray(response)) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
return response.map(parseFinding);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function executeSoloditSearch(
|
|
72
|
+
args: SoloditSearchArgs,
|
|
73
|
+
context: ToolContext,
|
|
74
|
+
callMcpTool?: CallMcpTool
|
|
75
|
+
): Promise<SoloditSearchResult> {
|
|
76
|
+
const { query } = args;
|
|
77
|
+
const limit = args.limit ?? DEFAULT_LIMIT;
|
|
78
|
+
|
|
79
|
+
context.metadata({ title: `Solodit search: ${query}` });
|
|
80
|
+
|
|
81
|
+
const mcpCaller =
|
|
82
|
+
callMcpTool ?? (hasMcpCapability(context) ? context.callMcpTool : undefined);
|
|
83
|
+
|
|
84
|
+
if (!mcpCaller) {
|
|
85
|
+
return {
|
|
86
|
+
results: [],
|
|
87
|
+
totalFound: 0,
|
|
88
|
+
query,
|
|
89
|
+
error: `Solodit MCP not available. Add to opencode.json mcp section or ensure solodit-mcp is running. Use @solodit-mcp directly: search_findings({query: '${query}', limit: ${limit}})`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const mcpArgs: Record<string, unknown> = { query, limit };
|
|
95
|
+
|
|
96
|
+
if (args.severity && args.severity.length > 0) {
|
|
97
|
+
mcpArgs.filters = { severity: args.severity };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const response = await mcpCaller(SOLODIT_MCP_SERVER, SOLODIT_MCP_TOOL, mcpArgs);
|
|
101
|
+
const findings = parseFindings(response);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
results: findings,
|
|
105
|
+
totalFound: findings.length,
|
|
106
|
+
query,
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
110
|
+
return {
|
|
111
|
+
results: [],
|
|
112
|
+
totalFound: 0,
|
|
113
|
+
query,
|
|
114
|
+
error: `Solodit MCP error: ${message}`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const soloditSearchTool = tool({
|
|
120
|
+
description:
|
|
121
|
+
"Search Solodit audit findings database for known vulnerabilities and past audit results via the Solodit MCP server.",
|
|
122
|
+
args: {
|
|
123
|
+
query: tool.schema.string(),
|
|
124
|
+
severity: tool.schema.array(tool.schema.string()).optional(),
|
|
125
|
+
limit: tool.schema.number().optional(),
|
|
126
|
+
},
|
|
127
|
+
async execute(args, context) {
|
|
128
|
+
const result = await executeSoloditSearch(args, context);
|
|
129
|
+
return JSON.stringify(result);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import os from "node:os"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { tool, type ToolContext } from "@opencode-ai/plugin"
|
|
4
|
+
import { ScvdClient } from "../knowledge/scvd-client"
|
|
5
|
+
import { syncAll, syncIncremental, type SyncResult } from "../knowledge/scvd-sync"
|
|
6
|
+
import { loadArgusConfig } from "../config/loader"
|
|
7
|
+
import type { ArgusConfig } from "../config/types"
|
|
8
|
+
|
|
9
|
+
type SyncKnowledgeArgs = {
|
|
10
|
+
force?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type SyncKnowledgeResult = {
|
|
14
|
+
success: boolean
|
|
15
|
+
scvd?: {
|
|
16
|
+
newFindings: number
|
|
17
|
+
totalIndexed: number
|
|
18
|
+
lastSync: string
|
|
19
|
+
}
|
|
20
|
+
errors?: string[]
|
|
21
|
+
error?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type SyncKnowledgeDependencies = {
|
|
25
|
+
loadConfig?: (projectDir: string) => ArgusConfig
|
|
26
|
+
createClient?: (apiUrl: string, signal: AbortSignal) => unknown
|
|
27
|
+
syncAllFn?: (client: unknown, indexPath: string) => Promise<SyncResult>
|
|
28
|
+
syncIncrementalFn?: (client: unknown, indexPath: string) => Promise<SyncResult>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DEFAULT_SCVD_API_URL = "https://api.scvd.dev"
|
|
32
|
+
|
|
33
|
+
function defaultDependencies(): Required<SyncKnowledgeDependencies> {
|
|
34
|
+
return {
|
|
35
|
+
loadConfig: loadArgusConfig,
|
|
36
|
+
createClient: (apiUrl: string, signal: AbortSignal) => new ScvdClient(apiUrl, signal),
|
|
37
|
+
syncAllFn: async (client: unknown, indexPath: string) =>
|
|
38
|
+
syncAll(client as ScvdClient, indexPath),
|
|
39
|
+
syncIncrementalFn: async (client: unknown, indexPath: string) =>
|
|
40
|
+
syncIncremental(client as ScvdClient, indexPath),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildSuccessResult(result: SyncResult): SyncKnowledgeResult {
|
|
45
|
+
const errors = result.error ? [result.error] : []
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
success: result.success,
|
|
49
|
+
scvd: {
|
|
50
|
+
newFindings: result.newFindings,
|
|
51
|
+
totalIndexed: result.totalIndexed,
|
|
52
|
+
lastSync: result.lastSync,
|
|
53
|
+
},
|
|
54
|
+
errors,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function toErrorMessage(error: unknown): string {
|
|
59
|
+
return error instanceof Error ? error.message : "Unknown SCVD sync error"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function executeSyncKnowledge(
|
|
63
|
+
args: SyncKnowledgeArgs,
|
|
64
|
+
context: ToolContext,
|
|
65
|
+
deps: SyncKnowledgeDependencies = {}
|
|
66
|
+
): Promise<SyncKnowledgeResult> {
|
|
67
|
+
const dependencies = { ...defaultDependencies(), ...deps }
|
|
68
|
+
|
|
69
|
+
context.metadata({ title: "Syncing SCVD knowledge index..." })
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const projectDir = context.directory ?? context.worktree ?? process.cwd()
|
|
73
|
+
const argusConfig = dependencies.loadConfig(projectDir)
|
|
74
|
+
|
|
75
|
+
if (!argusConfig.knowledge?.scvd?.enabled) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
error: "SCVD sync disabled in config",
|
|
79
|
+
errors: ["SCVD sync disabled in config"],
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const apiUrl = argusConfig.knowledge?.scvd?.apiUrl ?? DEFAULT_SCVD_API_URL
|
|
84
|
+
const indexPath = path.join(
|
|
85
|
+
os.homedir(),
|
|
86
|
+
".cache",
|
|
87
|
+
"solidity-argus",
|
|
88
|
+
"scvd-index.json"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const client = dependencies.createClient(apiUrl, context.abort)
|
|
92
|
+
const result = args.force
|
|
93
|
+
? await dependencies.syncAllFn(client, indexPath)
|
|
94
|
+
: await dependencies.syncIncrementalFn(client, indexPath)
|
|
95
|
+
|
|
96
|
+
return buildSuccessResult(result)
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const message = toErrorMessage(error)
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
error: message,
|
|
102
|
+
errors: [message],
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const syncKnowledgeTool = tool({
|
|
108
|
+
description: "Sync SCVD knowledge index to local cache for pattern-aware matching.",
|
|
109
|
+
args: {
|
|
110
|
+
force: tool.schema.boolean().default(false),
|
|
111
|
+
},
|
|
112
|
+
async execute(args, context) {
|
|
113
|
+
const result = await executeSyncKnowledge(args, context)
|
|
114
|
+
return JSON.stringify(result)
|
|
115
|
+
},
|
|
116
|
+
})
|