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