solidity-argus 0.2.0 → 0.3.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.
Files changed (167) hide show
  1. package/AGENTS.md +3 -3
  2. package/README.md +93 -37
  3. package/package.json +33 -7
  4. package/skills/INVENTORY.md +88 -57
  5. package/skills/README.md +26 -23
  6. package/skills/case-studies/beanstalk-governance/SKILL.md +52 -0
  7. package/skills/case-studies/bzx-flash-loan/SKILL.md +53 -0
  8. package/skills/case-studies/cream-finance/SKILL.md +52 -0
  9. package/skills/case-studies/curve-reentrancy/SKILL.md +52 -0
  10. package/skills/case-studies/dao-hack/SKILL.md +51 -0
  11. package/skills/case-studies/euler-finance/SKILL.md +52 -0
  12. package/skills/case-studies/harvest-finance/SKILL.md +52 -0
  13. package/skills/case-studies/level-finance/SKILL.md +51 -0
  14. package/skills/case-studies/mango-markets/SKILL.md +53 -0
  15. package/skills/case-studies/nomad-bridge/SKILL.md +51 -0
  16. package/skills/case-studies/parity-multisig/SKILL.md +55 -0
  17. package/skills/case-studies/poly-network/SKILL.md +51 -0
  18. package/skills/case-studies/rari-fuse/SKILL.md +51 -0
  19. package/skills/case-studies/ronin-bridge/SKILL.md +52 -0
  20. package/skills/case-studies/wormhole-bridge/SKILL.md +51 -0
  21. package/skills/manifests/smartbugs.json +1 -3
  22. package/skills/manifests/sunweb3sec.json +1 -3
  23. package/skills/vulnerability-patterns/access-control/SKILL.md +14 -0
  24. package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
  25. package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
  26. package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
  27. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +2 -1
  28. package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
  29. package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
  30. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +2 -1
  31. package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
  32. package/skills/vulnerability-patterns/dos-revert/SKILL.md +1 -0
  33. package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
  34. package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
  35. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +1 -0
  36. package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
  37. package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
  38. package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
  39. package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
  40. package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
  41. package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
  42. package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
  43. package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
  44. package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
  45. package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
  46. package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
  47. package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
  48. package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
  49. package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
  50. package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
  51. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +9 -0
  52. package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
  53. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +1 -0
  54. package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
  55. package/skills/vulnerability-patterns/reentrancy/SKILL.md +9 -0
  56. package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
  57. package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
  58. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +2 -1
  59. package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
  60. package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
  61. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +2 -1
  62. package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
  63. package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
  64. package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
  65. package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
  66. package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
  67. package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
  68. package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
  69. package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
  70. package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
  71. package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
  72. package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
  73. package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
  74. package/src/agents/argus-prompt.ts +24 -7
  75. package/src/agents/pythia-prompt.ts +3 -4
  76. package/src/agents/scribe-prompt.ts +7 -2
  77. package/src/agents/sentinel-prompt.ts +32 -3
  78. package/src/cli/cli-program.ts +29 -26
  79. package/src/cli/commands/check-skills.ts +135 -0
  80. package/src/cli/commands/doctor.ts +48 -26
  81. package/src/cli/commands/init.ts +5 -3
  82. package/src/cli/commands/install.ts +7 -5
  83. package/src/cli/commands/lint-skills.ts +16 -12
  84. package/src/cli/index.ts +5 -5
  85. package/src/cli/types.ts +3 -3
  86. package/src/config/index.ts +1 -1
  87. package/src/config/loader.ts +4 -6
  88. package/src/config/schema.ts +4 -5
  89. package/src/config/types.ts +2 -2
  90. package/src/constants/defaults.ts +2 -0
  91. package/src/create-hooks.ts +145 -34
  92. package/src/create-managers.ts +10 -8
  93. package/src/create-tools.ts +13 -9
  94. package/src/features/background-agent/background-manager.ts +93 -87
  95. package/src/features/background-agent/index.ts +1 -1
  96. package/src/features/context-monitor/context-monitor.ts +3 -3
  97. package/src/features/context-monitor/index.ts +2 -2
  98. package/src/features/error-recovery/session-recovery.ts +2 -4
  99. package/src/features/error-recovery/tool-error-recovery.ts +12 -7
  100. package/src/features/index.ts +5 -5
  101. package/src/features/persistent-state/audit-state-manager.ts +143 -60
  102. package/src/features/persistent-state/global-run-index.ts +38 -0
  103. package/src/features/persistent-state/index.ts +1 -1
  104. package/src/features/persistent-state/run-journal.ts +86 -0
  105. package/src/hooks/config-handler.ts +28 -11
  106. package/src/hooks/context-budget.ts +2 -5
  107. package/src/hooks/event-hook.ts +47 -23
  108. package/src/hooks/hook-system.ts +4 -4
  109. package/src/hooks/index.ts +5 -5
  110. package/src/hooks/knowledge-sync-hook.ts +18 -21
  111. package/src/hooks/recon-context-builder.ts +2 -2
  112. package/src/hooks/safe-create-hook.ts +6 -7
  113. package/src/hooks/tool-tracking-hook.ts +104 -50
  114. package/src/hooks/types.ts +2 -1
  115. package/src/index.ts +23 -36
  116. package/src/knowledge/retry.ts +22 -22
  117. package/src/knowledge/scvd-client.ts +88 -95
  118. package/src/knowledge/scvd-errors.ts +35 -35
  119. package/src/knowledge/scvd-index.ts +78 -80
  120. package/src/knowledge/scvd-sync.ts +106 -101
  121. package/src/managers/index.ts +1 -1
  122. package/src/managers/types.ts +19 -14
  123. package/src/plugin-interface.ts +7 -9
  124. package/src/shared/binary-utils.ts +44 -35
  125. package/src/shared/deep-merge.ts +55 -36
  126. package/src/shared/file-utils.ts +21 -19
  127. package/src/shared/index.ts +11 -5
  128. package/src/shared/jsonc-parser.ts +123 -28
  129. package/src/shared/logger.ts +16 -3
  130. package/src/shared/project-utils.ts +30 -0
  131. package/src/skills/analysis/cluster.ts +414 -0
  132. package/src/skills/analysis/gates.ts +227 -0
  133. package/src/skills/analysis/index.ts +33 -0
  134. package/src/skills/analysis/normalize.ts +217 -0
  135. package/src/skills/analysis/similarity.ts +224 -0
  136. package/src/skills/argus-skill-resolver.ts +17 -6
  137. package/src/skills/skill-schema.ts +11 -10
  138. package/src/solodit-lifecycle.ts +202 -0
  139. package/src/state/audit-state.ts +8 -8
  140. package/src/state/finding-store.ts +68 -55
  141. package/src/state/types.ts +88 -67
  142. package/src/tools/argus-skill-load-tool.ts +12 -7
  143. package/src/tools/contract-analyzer-tool.ts +60 -77
  144. package/src/tools/forge-coverage-tool.ts +226 -0
  145. package/src/tools/forge-fuzz-tool.ts +127 -127
  146. package/src/tools/forge-test-tool.ts +153 -157
  147. package/src/tools/gas-analysis-tool.ts +264 -0
  148. package/src/tools/pattern-checker-tool.ts +185 -190
  149. package/src/tools/pattern-loader.ts +5 -111
  150. package/src/tools/proxy-detection-tool.ts +224 -0
  151. package/src/tools/report-generator-tool.ts +268 -200
  152. package/src/tools/slither-tool.ts +266 -218
  153. package/src/tools/solodit-search-tool.ts +216 -119
  154. package/src/tools/sync-knowledge-tool.ts +7 -11
  155. package/src/utils/audit-artifact-detector.ts +28 -29
  156. package/src/utils/dependency-scanner.ts +37 -37
  157. package/src/utils/project-detector.ts +111 -124
  158. package/src/utils/solidity-parser.ts +103 -74
  159. package/skills/patterns/access-control.yaml +0 -31
  160. package/skills/patterns/erc4626.yaml +0 -29
  161. package/skills/patterns/flash-loan.yaml +0 -20
  162. package/skills/patterns/oracle.yaml +0 -30
  163. package/skills/patterns/proxy.yaml +0 -30
  164. package/skills/patterns/reentrancy.yaml +0 -30
  165. package/skills/patterns/signature.yaml +0 -31
  166. package/src/hooks/event-hook-v2.ts +0 -99
  167. package/src/state/plugin-state.ts +0 -14
@@ -1,133 +1,134 @@
1
- import { createHash } from "crypto";
2
- import { mkdtempSync, writeFileSync, rmSync, readFileSync, existsSync } from "node:fs";
3
- import { join, resolve, dirname, isAbsolute } 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";
1
+ import { createHash } from "node:crypto"
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
3
+ import { tmpdir } from "node:os"
4
+ import { dirname, isAbsolute, join, resolve } from "node:path"
5
+ import { type ToolContext, tool } from "@opencode-ai/plugin"
6
+ import { createLogger } from "../shared/logger"
7
+ import type { Finding, FindingSeverity } from "../state/types"
8
+
9
+ const logger = createLogger()
10
+
11
+ import {
12
+ extractContractNames as extractContractNamesShared,
13
+ hasBinary as hasBinaryShared,
14
+ parseSolcVersion as parseSolcVersionShared,
15
+ } from "../shared/binary-utils"
16
+ import { resolveProjectDir } from "../shared/project-utils"
9
17
 
10
18
  type SlitherArgs = {
11
- target: string;
12
- detectors?: string[];
13
- exclude?: string[];
14
- solc_version?: string;
15
- via_ir?: boolean;
16
- };
19
+ target: string
20
+ detectors?: string[]
21
+ exclude?: string[]
22
+ solc_version?: string
23
+ via_ir?: boolean
24
+ }
17
25
 
18
26
  type SlitherDetector = {
19
- check?: string;
20
- impact?: string;
21
- confidence?: string;
22
- description?: string;
27
+ check?: string
28
+ impact?: string
29
+ confidence?: string
30
+ description?: string
23
31
  elements?: Array<{
24
32
  source_mapping?: {
25
- filename_relative?: string;
26
- lines?: number[];
27
- };
28
- }>;
29
- };
33
+ filename_relative?: string
34
+ lines?: number[]
35
+ }
36
+ }>
37
+ }
30
38
 
31
39
  type SlitherPayload = {
32
- success?: boolean;
33
- error?: string | null;
40
+ success?: boolean
41
+ error?: string | null
34
42
  results?: {
35
- detectors?: SlitherDetector[];
36
- };
37
- };
43
+ detectors?: SlitherDetector[]
44
+ }
45
+ }
38
46
 
39
47
  export type SlitherRunResult = {
40
- stdout: string;
41
- stderr: string;
42
- exitCode: number;
43
- };
48
+ stdout: string
49
+ stderr: string
50
+ exitCode: number
51
+ }
44
52
 
45
53
  export type RunSlitherCommand = (
46
54
  command: string[],
47
55
  signal: AbortSignal,
48
- cwd: string
49
- ) => Promise<SlitherRunResult>;
56
+ cwd: string,
57
+ ) => Promise<SlitherRunResult>
50
58
 
51
59
  export type SlitherAnalyzeResult = {
52
- success: boolean;
53
- findingsCount: number;
54
- findings: Finding[];
55
- executionTime: number;
56
- errors: string[];
57
- error?: string;
58
- };
60
+ success: boolean
61
+ findingsCount: number
62
+ findings: Finding[]
63
+ executionTime: number
64
+ errors: string[]
65
+ error?: string
66
+ }
59
67
 
60
68
  function mapSeverity(impact?: string): FindingSeverity {
61
69
  switch (impact) {
62
70
  case "High":
63
- return "High";
71
+ return "High"
64
72
  case "Medium":
65
- return "Medium";
73
+ return "Medium"
66
74
  case "Low":
67
- return "Low";
75
+ return "Low"
68
76
  case "Informational":
69
- return "Informational";
77
+ return "Informational"
70
78
  default:
71
- return "Informational";
79
+ return "Informational"
72
80
  }
73
81
  }
74
82
 
75
83
  function mapConfidence(confidence?: string): "High" | "Medium" | "Low" {
76
84
  switch (confidence) {
77
85
  case "High":
78
- return "High";
86
+ return "High"
79
87
  case "Medium":
80
- return "Medium";
88
+ return "Medium"
81
89
  case "Low":
82
- return "Low";
90
+ return "Low"
83
91
  default:
84
- return "Low";
92
+ return "Low"
85
93
  }
86
94
  }
87
95
 
88
96
  function findingLines(lines?: number[]): [number, number] {
89
97
  if (!lines || lines.length === 0) {
90
- return [1, 1];
98
+ return [1, 1]
91
99
  }
92
100
 
93
101
  if (lines.length === 1) {
94
- const only = lines[0] ?? 1;
95
- return [only, only];
102
+ const only = lines[0] ?? 1
103
+ return [only, only]
96
104
  }
97
105
 
98
- const start = lines[0] ?? 1;
99
- const end = lines[lines.length - 1] ?? start;
100
- return [start, end];
106
+ const start = lines[0] ?? 1
107
+ const end = lines[lines.length - 1] ?? start
108
+ return [start, end]
101
109
  }
102
110
 
103
111
  function createFindingID(check: string, file: string, lines: [number, number]): string {
104
- const key = `${check}:${file}:${lines[0]}-${lines[1]}`;
105
- return createHash("sha256").update(key).digest("hex").slice(0, 16);
112
+ const key = `${check}:${file}:${lines[0]}-${lines[1]}`
113
+ return createHash("sha256").update(key).digest("hex").slice(0, 16)
106
114
  }
107
115
 
108
116
  function buildCommand(args: SlitherArgs): string[] {
109
- const command = [
110
- "slither",
111
- args.target,
112
- "--json",
113
- "-",
114
- "--filter-paths",
115
- "node_modules",
116
- ];
117
+ const command = ["slither", args.target, "--json", "-", "--filter-paths", "node_modules"]
117
118
 
118
119
  if (args.detectors && args.detectors.length > 0) {
119
- command.push("--detect", args.detectors.join(","));
120
+ command.push("--detect", args.detectors.join(","))
120
121
  }
121
122
 
122
123
  if (args.exclude && args.exclude.length > 0) {
123
- command.push("--exclude-detectors", args.exclude.join(","));
124
+ command.push("--exclude-detectors", args.exclude.join(","))
124
125
  }
125
126
 
126
127
  if (args.solc_version) {
127
- command.push("--solc", `solc:${args.solc_version}`);
128
+ command.push("--solc", `solc:${args.solc_version}`)
128
129
  }
129
130
 
130
- return command;
131
+ return command
131
132
  }
132
133
 
133
134
  const FALLBACK_TRIGGERS = [
@@ -143,28 +144,38 @@ const FALLBACK_TRIGGERS = [
143
144
  "YulException",
144
145
  "StackTooDeep",
145
146
  "Stack too deep",
146
- ];
147
+ ]
147
148
 
148
149
  function shouldTryFlattenFallback(errors: string[], stderr: string): boolean {
149
- const combined = [...errors, stderr].join(" ");
150
- return FALLBACK_TRIGGERS.some((trigger) => combined.includes(trigger));
150
+ const combined = [...errors, stderr].join(" ")
151
+ return FALLBACK_TRIGGERS.some((trigger) => combined.includes(trigger))
151
152
  }
152
153
 
153
154
  const parseSolcVersion = parseSolcVersionShared
154
155
  const extractContractNames = extractContractNamesShared
155
156
  const hasBinary = hasBinaryShared
156
157
 
157
- function ensureSolc(version: string): boolean {
158
- if (hasBinary("solc")) return true;
159
- if (!hasBinary("solc-select")) return false;
158
+ async function ensureSolc(version: string): Promise<boolean> {
159
+ if (hasBinary("solc")) return true
160
+ if (!hasBinary("solc-select")) return false
160
161
  try {
161
- execSync(`solc-select install ${version} && solc-select use ${version}`, {
162
- stdio: "ignore",
163
- timeout: 60_000,
164
- });
165
- return true;
162
+ const installProc = Bun.spawn(["solc-select", "install", version], {
163
+ stdout: "pipe",
164
+ stderr: "pipe",
165
+ signal: AbortSignal.timeout(30_000),
166
+ })
167
+ const installExit = await installProc.exited
168
+ if (installExit !== 0) return false
169
+
170
+ const useProc = Bun.spawn(["solc-select", "use", version], {
171
+ stdout: "pipe",
172
+ stderr: "pipe",
173
+ signal: AbortSignal.timeout(30_000),
174
+ })
175
+ const useExit = await useProc.exited
176
+ return useExit === 0
166
177
  } catch (_e) {
167
- return false;
178
+ return false
168
179
  }
169
180
  }
170
181
 
@@ -174,30 +185,50 @@ export const runSlitherCommand: RunSlitherCommand = async (command, signal, cwd)
174
185
  stdout: "pipe",
175
186
  stderr: "pipe",
176
187
  signal,
177
- });
188
+ })
178
189
 
179
190
  const [exitCode, stdout, stderr] = await Promise.all([
180
191
  child.exited,
181
192
  new Response(child.stdout).text(),
182
193
  new Response(child.stderr).text(),
183
- ]);
194
+ ])
184
195
 
185
196
  return {
186
197
  stdout,
187
198
  stderr,
188
199
  exitCode,
189
- };
190
- };
200
+ }
201
+ }
202
+
203
+ export type SpawnFn = (
204
+ command: string[],
205
+ options?: { cwd?: string; timeout?: number },
206
+ ) => Promise<{ stdout: string; exitCode: number }>
191
207
 
192
208
  export type FlattenFallbackDeps = {
193
- runCommand: RunSlitherCommand;
194
- hasBinary: (name: string) => boolean;
195
- ensureSolc: (version: string) => boolean;
196
- parseSolcVersion: (target: string) => string | undefined;
197
- extractContractNames: (filePath: string) => string[];
198
- execSyncFn: typeof execSync;
199
- cwd: string;
200
- };
209
+ runCommand: RunSlitherCommand
210
+ hasBinary: (name: string) => boolean
211
+ ensureSolc: (version: string) => Promise<boolean>
212
+ parseSolcVersion: (target: string) => Promise<string | undefined> | string | undefined
213
+ extractContractNames: (filePath: string) => Promise<string[]> | string[]
214
+ spawnFn: SpawnFn
215
+ cwd: string
216
+ }
217
+
218
+ async function defaultSpawnFn(
219
+ command: string[],
220
+ options?: { cwd?: string; timeout?: number },
221
+ ): Promise<{ stdout: string; exitCode: number }> {
222
+ const proc = Bun.spawn(command, {
223
+ stdout: "pipe",
224
+ stderr: "pipe",
225
+ cwd: options?.cwd,
226
+ ...(options?.timeout ? { signal: AbortSignal.timeout(options.timeout) } : {}),
227
+ })
228
+ const exitCode = await proc.exited
229
+ const stdout = await new Response(proc.stdout).text()
230
+ return { stdout, exitCode }
231
+ }
201
232
 
202
233
  const defaultFlattenDeps: FlattenFallbackDeps = {
203
234
  runCommand: runSlitherCommand,
@@ -205,16 +236,16 @@ const defaultFlattenDeps: FlattenFallbackDeps = {
205
236
  ensureSolc,
206
237
  parseSolcVersion,
207
238
  extractContractNames,
208
- execSyncFn: execSync,
239
+ spawnFn: defaultSpawnFn,
209
240
  cwd: process.cwd(),
210
- };
241
+ }
211
242
 
212
243
  export async function flattenFallback(
213
244
  args: SlitherArgs,
214
245
  context: ToolContext,
215
246
  deps: FlattenFallbackDeps = defaultFlattenDeps,
216
247
  ): Promise<SlitherAnalyzeResult | undefined> {
217
- const startedAt = Date.now();
248
+ const startedAt = Date.now()
218
249
 
219
250
  if (!deps.hasBinary("forge")) {
220
251
  return {
@@ -224,117 +255,129 @@ export async function flattenFallback(
224
255
  executionTime: Date.now() - startedAt,
225
256
  errors: ["forge binary not found — required for via_ir flatten fallback"],
226
257
  error: "forge binary not found — required for via_ir flatten fallback",
227
- };
258
+ }
228
259
  }
229
260
 
230
- const solcVersion = args.solc_version ?? deps.parseSolcVersion(args.target);
261
+ const solcVersion = args.solc_version ?? (await deps.parseSolcVersion(args.target))
231
262
  if (!solcVersion) {
232
263
  return {
233
264
  success: false,
234
265
  findingsCount: 0,
235
266
  findings: [],
236
267
  executionTime: Date.now() - startedAt,
237
- errors: ["Could not determine solc version from foundry.toml or pragma — required for flatten fallback"],
238
- error: "Could not determine solc version from foundry.toml or pragma — required for flatten fallback",
239
- };
268
+ errors: [
269
+ "Could not determine solc version from foundry.toml or pragma — required for flatten fallback",
270
+ ],
271
+ error:
272
+ "Could not determine solc version from foundry.toml or pragma — required for flatten fallback",
273
+ }
240
274
  }
241
275
 
242
- if (!deps.ensureSolc(solcVersion)) {
276
+ if (!(await deps.ensureSolc(solcVersion))) {
243
277
  return {
244
278
  success: false,
245
279
  findingsCount: 0,
246
280
  findings: [],
247
281
  executionTime: Date.now() - startedAt,
248
282
  errors: ["solc not available and solc-select not found"],
249
- error: "Flatten fallback requires solc on PATH. Install with: pipx install solc-select && solc-select install " + solcVersion,
250
- };
283
+ error:
284
+ "Flatten fallback requires solc on PATH. Install with: pipx install solc-select && solc-select install " +
285
+ solcVersion,
286
+ }
251
287
  }
252
288
 
253
- const srcDir = join(args.target, "src");
254
- let solFiles: string[] = [];
289
+ const srcDir = join(args.target, "src")
290
+ let solFiles: string[] = []
255
291
  if (args.target.endsWith(".sol")) {
256
- solFiles = [args.target];
292
+ solFiles = [args.target]
257
293
  } else if (existsSync(srcDir)) {
258
294
  try {
259
- solFiles = deps.execSyncFn(`find "${srcDir}" -name "*.sol" -maxdepth 3 -not -path "*/mocks/*" -not -path "*/test/*"`, {
260
- encoding: "utf-8",
261
- timeout: 5_000,
262
- stdio: ["pipe", "pipe", "pipe"],
263
- })
264
- .trim()
265
- .split("\n")
266
- .filter(Boolean);
295
+ const findResult = await deps.spawnFn(
296
+ [
297
+ "find",
298
+ srcDir,
299
+ "-maxdepth",
300
+ "3",
301
+ "-name",
302
+ "*.sol",
303
+ "-not",
304
+ "-path",
305
+ "*/mocks/*",
306
+ "-not",
307
+ "-path",
308
+ "*/test/*",
309
+ ],
310
+ { timeout: 5_000 },
311
+ )
312
+ if (findResult.exitCode !== 0) return undefined
313
+ solFiles = findResult.stdout.trim().split("\n").filter(Boolean)
267
314
  } catch (_e) {
268
- return undefined;
315
+ return undefined
269
316
  }
270
317
  }
271
318
 
272
- if (solFiles.length === 0) return undefined;
319
+ if (solFiles.length === 0) return undefined
273
320
 
274
- const tmpDir = mkdtempSync(join(tmpdir(), "argus-slither-"));
275
- const allFindings: Finding[] = [];
276
- const errors: string[] = [];
321
+ const tmpDir = mkdtempSync(join(tmpdir(), "argus-slither-"))
322
+ const allFindings: Finding[] = []
323
+ const errors: string[] = []
277
324
 
278
325
  try {
279
326
  for (const solFile of solFiles) {
280
- if (context.abort.aborted) break;
327
+ if (context.abort.aborted) break
281
328
 
282
- const baseName = solFile.split("/").pop()?.replace(".sol", "") ?? "Contract";
283
- const flatFile = join(tmpDir, `${baseName}.flat.sol`);
284
- const originalContracts = deps.extractContractNames(solFile);
329
+ const baseName = solFile.split("/").pop()?.replace(".sol", "") ?? "Contract"
330
+ const flatFile = join(tmpDir, `${baseName}.flat.sol`)
331
+ const originalContracts = await deps.extractContractNames(solFile)
285
332
 
286
333
  try {
287
- const flattened = deps.execSyncFn(`forge flatten "${solFile}"`, {
288
- encoding: "utf-8",
289
- timeout: 30_000,
334
+ const flatResult = await deps.spawnFn(["forge", "flatten", solFile], {
290
335
  cwd: deps.cwd,
291
- stdio: ["pipe", "pipe", "pipe"],
292
- });
293
- writeFileSync(flatFile, flattened);
336
+ timeout: 30_000,
337
+ })
338
+ if (flatResult.exitCode !== 0) {
339
+ errors.push(`forge flatten failed for ${solFile}`)
340
+ continue
341
+ }
342
+ writeFileSync(flatFile, flatResult.stdout)
294
343
  } catch (_e) {
295
- errors.push(`forge flatten failed for ${solFile}`);
296
- continue;
344
+ errors.push(`forge flatten failed for ${solFile}`)
345
+ continue
297
346
  }
298
347
 
299
- const command = [
300
- "slither",
301
- flatFile,
302
- "--json",
303
- "-",
304
- "--solc-solcs-select",
305
- solcVersion,
306
- ];
348
+ const command = ["slither", flatFile, "--json", "-", "--solc-solcs-select", solcVersion]
307
349
 
308
350
  try {
309
- const runResult = await deps.runCommand(command, context.abort, deps.cwd);
351
+ const runResult = await deps.runCommand(command, context.abort, deps.cwd)
310
352
 
311
- let payload: SlitherPayload;
353
+ let payload: SlitherPayload
312
354
  try {
313
- payload = JSON.parse(runResult.stdout) as SlitherPayload;
355
+ payload = JSON.parse(runResult.stdout) as SlitherPayload
314
356
  } catch (_e) {
315
- if (runResult.stderr.trim()) errors.push(runResult.stderr.trim());
316
- continue;
357
+ if (runResult.stderr.trim()) errors.push(runResult.stderr.trim())
358
+ continue
317
359
  }
318
360
 
319
- const rawFindings = parseFindings(payload);
320
- const filtered = originalContracts.length > 0
321
- ? rawFindings.filter((f) => {
322
- if (f.file.includes(".flat.sol") || f.file === flatFile) return true;
323
- return originalContracts.some(
324
- (name) => f.description.includes(name) || f.file.includes(name)
325
- );
326
- })
327
- : rawFindings;
361
+ const rawFindings = parseFindings(payload)
362
+ const filtered =
363
+ originalContracts.length > 0
364
+ ? rawFindings.filter((f) => {
365
+ if (f.file.includes(".flat.sol") || f.file === flatFile) return true
366
+ return originalContracts.some(
367
+ (name) => f.description.includes(name) || f.file.includes(name),
368
+ )
369
+ })
370
+ : rawFindings
328
371
 
329
372
  const remapped = filtered.map((f) => ({
330
373
  ...f,
331
- file: f.file.includes(".flat.sol") ? solFile.replace(args.target + "/", "") : f.file,
332
- }));
374
+ file: f.file.includes(".flat.sol") ? solFile.replace(`${args.target}/`, "") : f.file,
375
+ }))
333
376
 
334
- allFindings.push(...remapped);
377
+ allFindings.push(...remapped)
335
378
  } catch (e) {
336
- const msg = e instanceof Error ? e.message : String(e);
337
- errors.push(`Slither flatten fallback failed for ${baseName}: ${msg}`);
379
+ const msg = e instanceof Error ? e.message : String(e)
380
+ errors.push(`Slither flatten fallback failed for ${baseName}: ${msg}`)
338
381
  }
339
382
  }
340
383
 
@@ -343,24 +386,27 @@ export async function flattenFallback(
343
386
  findingsCount: allFindings.length,
344
387
  findings: allFindings,
345
388
  executionTime: Date.now() - startedAt,
346
- errors: errors.length > 0 ? [`[flatten-fallback] ${errors.join("; ")}`] : ["[flatten-fallback] Analysis completed via forge flatten"],
347
- };
389
+ errors:
390
+ errors.length > 0
391
+ ? [`[flatten-fallback] ${errors.join("; ")}`]
392
+ : ["[flatten-fallback] Analysis completed via forge flatten"],
393
+ }
348
394
  } finally {
349
395
  try {
350
- rmSync(tmpDir, { recursive: true, force: true });
396
+ rmSync(tmpDir, { recursive: true, force: true })
351
397
  } catch (_cleanupErr) {
352
- // best-effort: temp dir cleanup failure is non-fatal
398
+ logger.debug("Failed to clean up temp directory")
353
399
  }
354
400
  }
355
401
  }
356
402
 
357
403
  function parseFindings(payload: SlitherPayload): Finding[] {
358
- const detectors = payload.results?.detectors ?? [];
404
+ const detectors = payload.results?.detectors ?? []
359
405
 
360
406
  return detectors.map((detector) => {
361
- const file = detector.elements?.[0]?.source_mapping?.filename_relative ?? "unknown";
362
- const lines = findingLines(detector.elements?.[0]?.source_mapping?.lines);
363
- const check = detector.check ?? "unknown-check";
407
+ const file = detector.elements?.[0]?.source_mapping?.filename_relative ?? "unknown"
408
+ const lines = findingLines(detector.elements?.[0]?.source_mapping?.lines)
409
+ const check = detector.check ?? "unknown-check"
364
410
 
365
411
  return {
366
412
  id: createFindingID(check, file, lines),
@@ -371,62 +417,65 @@ function parseFindings(payload: SlitherPayload): Finding[] {
371
417
  file,
372
418
  lines,
373
419
  source: "slither",
374
- };
375
- });
420
+ }
421
+ })
376
422
  }
377
423
 
378
424
  export async function executeSlitherAnalyze(
379
425
  args: SlitherArgs,
380
426
  context: ToolContext,
381
427
  runCommand: RunSlitherCommand = runSlitherCommand,
382
- cwd?: string
428
+ cwd?: string,
383
429
  ): Promise<SlitherAnalyzeResult> {
384
- const projectDir = cwd ?? context.directory ?? context.worktree ?? process.cwd();
385
- const startedAt = Date.now();
386
- context.metadata({ title: `Slither analysis: ${args.target}` });
430
+ const projectDir = cwd ?? resolveProjectDir(context)
431
+ const startedAt = Date.now()
432
+ context.metadata({ title: `Slither analysis: ${args.target}` })
387
433
 
388
434
  if (args.via_ir) {
389
435
  const fallbackResult = await flattenFallback(args, context, {
390
436
  ...defaultFlattenDeps,
391
437
  runCommand,
392
438
  cwd: projectDir,
393
- });
394
- if (fallbackResult) return fallbackResult;
439
+ })
440
+ if (fallbackResult) return fallbackResult
395
441
  return {
396
442
  success: false,
397
443
  findingsCount: 0,
398
444
  findings: [],
399
445
  executionTime: Date.now() - startedAt,
400
- errors: ["via_ir enabled — flatten fallback failed. Ensure forge and solc are available."],
401
- error: "Project uses via_ir which is incompatible with Slither direct analysis. Flatten fallback also failed.",
402
- };
446
+ errors: [
447
+ "via_ir enabled flatten fallback failed. Ensure forge and solc-select are installed.",
448
+ ],
449
+ error:
450
+ "Project uses via_ir which is incompatible with Slither direct analysis. Flatten fallback also failed.",
451
+ }
403
452
  }
404
453
 
405
- const command = buildCommand(args);
454
+ const command = buildCommand(args)
406
455
 
407
456
  try {
408
- const runResult = await runCommand(command, context.abort, projectDir);
409
- const errors: string[] = [];
457
+ const runResult = await runCommand(command, context.abort, projectDir)
458
+ const errors: string[] = []
410
459
 
411
460
  if (runResult.exitCode !== 0) {
412
- errors.push(`Slither exited with code ${runResult.exitCode}`);
461
+ errors.push(`Slither exited with code ${runResult.exitCode}`)
413
462
  }
414
463
  if (runResult.stderr.trim().length > 0) {
415
- errors.push(runResult.stderr.trim());
464
+ errors.push(runResult.stderr.trim())
416
465
  }
417
466
 
418
- let payload: SlitherPayload;
467
+ let payload: SlitherPayload
419
468
  try {
420
- payload = JSON.parse(runResult.stdout) as SlitherPayload;
469
+ payload = JSON.parse(runResult.stdout) as SlitherPayload
421
470
  } catch (error) {
422
- const message = error instanceof Error ? error.message : "Unknown parse error";
471
+ const message = error instanceof Error ? error.message : "Unknown parse error"
423
472
  if (shouldTryFlattenFallback(errors, runResult.stderr)) {
424
473
  const fallbackResult = await flattenFallback(args, context, {
425
474
  ...defaultFlattenDeps,
426
475
  runCommand,
427
476
  cwd: projectDir,
428
- });
429
- if (fallbackResult) return fallbackResult;
477
+ })
478
+ if (fallbackResult) return fallbackResult
430
479
  }
431
480
  return {
432
481
  success: false,
@@ -435,23 +484,23 @@ export async function executeSlitherAnalyze(
435
484
  executionTime: Date.now() - startedAt,
436
485
  errors,
437
486
  error: `Slither output parse error: ${message}`,
438
- };
487
+ }
439
488
  }
440
489
 
441
490
  if (payload.error) {
442
- errors.push(payload.error);
491
+ errors.push(payload.error)
443
492
  }
444
493
 
445
- const findings = parseFindings(payload);
446
- const success = findings.length > 0 || (runResult.exitCode === 0 && payload.success !== false);
494
+ const findings = parseFindings(payload)
495
+ const success = findings.length > 0 || (runResult.exitCode === 0 && payload.success !== false)
447
496
 
448
497
  if (!success && findings.length === 0 && shouldTryFlattenFallback(errors, runResult.stderr)) {
449
498
  const fallbackResult = await flattenFallback(args, context, {
450
499
  ...defaultFlattenDeps,
451
500
  runCommand,
452
501
  cwd: projectDir,
453
- });
454
- if (fallbackResult) return fallbackResult;
502
+ })
503
+ if (fallbackResult) return fallbackResult
455
504
  }
456
505
 
457
506
  return {
@@ -460,10 +509,10 @@ export async function executeSlitherAnalyze(
460
509
  findings,
461
510
  executionTime: Date.now() - startedAt,
462
511
  errors,
463
- };
512
+ }
464
513
  } catch (error) {
465
- const message = error instanceof Error ? error.message : "Unknown error";
466
- const maybeErrno = error as Error & { code?: string; name?: string };
514
+ const message = error instanceof Error ? error.message : "Unknown error"
515
+ const maybeErrno = error as Error & { code?: string; name?: string }
467
516
 
468
517
  if (maybeErrno.code === "ENOENT") {
469
518
  return {
@@ -473,7 +522,7 @@ export async function executeSlitherAnalyze(
473
522
  executionTime: Date.now() - startedAt,
474
523
  errors: [],
475
524
  error: "Slither not found. Install with: pip install slither-analyzer",
476
- };
525
+ }
477
526
  }
478
527
 
479
528
  if (maybeErrno.name === "AbortError" || context.abort.aborted) {
@@ -484,7 +533,7 @@ export async function executeSlitherAnalyze(
484
533
  executionTime: Date.now() - startedAt,
485
534
  errors: ["Slither analysis aborted"],
486
535
  error: "Slither analysis aborted",
487
- };
536
+ }
488
537
  }
489
538
 
490
539
  return {
@@ -494,34 +543,33 @@ export async function executeSlitherAnalyze(
494
543
  executionTime: Date.now() - startedAt,
495
544
  errors: [message],
496
545
  error: message,
497
- };
546
+ }
498
547
  }
499
548
  }
500
549
 
501
550
  export function detectViaIr(target: string): boolean {
502
- let dir = resolve(target.endsWith(".sol") ? dirname(target) : target);
503
- const root = resolve("/");
551
+ let dir = resolve(target.endsWith(".sol") ? dirname(target) : target)
552
+ const root = resolve("/")
504
553
 
505
554
  while (true) {
506
- const foundryTomlPath = join(dir, "foundry.toml");
555
+ const foundryTomlPath = join(dir, "foundry.toml")
507
556
  if (existsSync(foundryTomlPath)) {
508
557
  try {
509
- const content = readFileSync(foundryTomlPath, "utf-8");
510
- if (/^\s*via[_-]ir\s*=\s*true/m.test(content)) return true;
558
+ const content = readFileSync(foundryTomlPath, "utf-8")
559
+ if (/^\s*via[_-]ir\s*=\s*true/m.test(content)) return true
511
560
  } catch {
512
- // unreadable file keep walking
561
+ logger.debug("Unreadable foundry.toml, continuing directory walk")
513
562
  }
514
563
  }
515
- if (dir === root) break;
516
- dir = dirname(dir);
564
+ if (dir === root) break
565
+ dir = dirname(dir)
517
566
  }
518
567
 
519
- return false;
568
+ return false
520
569
  }
521
570
 
522
571
  export const slitherTool = tool({
523
- description:
524
- "Run Slither static analysis and return normalized findings for Solidity targets.",
572
+ description: "Run Slither static analysis and return normalized findings for Solidity targets.",
525
573
  args: {
526
574
  target: tool.schema.string(),
527
575
  detectors: tool.schema.array(tool.schema.string()).optional(),
@@ -530,15 +578,15 @@ export const slitherTool = tool({
530
578
  via_ir: tool.schema.boolean().optional(),
531
579
  },
532
580
  async execute(args, context) {
533
- const projectDir = context.directory ?? context.worktree ?? process.cwd();
534
- const resolvedTarget = isAbsolute(args.target) ? args.target : resolve(projectDir, args.target);
535
- const viaIr = args.via_ir ?? detectViaIr(resolvedTarget);
581
+ const projectDir = resolveProjectDir(context)
582
+ const resolvedTarget = isAbsolute(args.target) ? args.target : resolve(projectDir, args.target)
583
+ const viaIr = args.via_ir ?? detectViaIr(resolvedTarget)
536
584
  const result = await executeSlitherAnalyze(
537
585
  { ...args, target: resolvedTarget, via_ir: viaIr },
538
586
  context,
539
587
  runSlitherCommand,
540
588
  projectDir,
541
- );
542
- return JSON.stringify(result);
589
+ )
590
+ return JSON.stringify(result)
543
591
  },
544
- });
592
+ })