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,41 +1,47 @@
1
- import { tool, type ToolContext } from "@opencode-ai/plugin";
1
+ import type { ToolDefinition } from "@opencode-ai/plugin"
2
+ import { type ToolContext, tool } from "@opencode-ai/plugin"
3
+ import { createLogger } from "../shared/logger"
2
4
 
3
- const SOLODIT_MCP_SERVER = "solodit-mcp";
4
- const SOLODIT_MCP_TOOL = "search_findings";
5
- const DEFAULT_LIMIT = 10;
5
+ const logger = createLogger()
6
+
7
+ const SOLODIT_MCP_SERVER = "solodit-mcp"
8
+ const SOLODIT_MCP_TOOLS = ["search", "search_findings"] as const
9
+ const DEFAULT_LIMIT = 10
10
+ const DEFAULT_SOLODIT_PORT = 3000
11
+ const SOLODIT_HTTP_TIMEOUT_MS = 10_000
6
12
 
7
13
  type SoloditSearchArgs = {
8
- query: string;
9
- severity?: string[];
10
- limit?: number;
11
- };
14
+ query: string
15
+ severity?: string[]
16
+ limit?: number
17
+ }
12
18
 
13
19
  type SoloditFinding = {
14
- title: string;
15
- severity: string;
16
- description: string;
17
- protocol: string;
18
- url: string;
19
- remediation: string;
20
- };
20
+ title: string
21
+ severity: string
22
+ description: string
23
+ protocol: string
24
+ url: string
25
+ remediation: string
26
+ }
21
27
 
22
28
  export type SoloditSearchResult = {
23
- results: SoloditFinding[];
24
- totalFound: number;
25
- query: string;
26
- error?: string;
27
- };
29
+ results: SoloditFinding[]
30
+ totalFound: number
31
+ query: string
32
+ error?: string
33
+ }
28
34
 
29
35
  export type CallMcpTool = (
30
36
  server: string,
31
37
  tool: string,
32
- args: Record<string, unknown>
33
- ) => Promise<unknown>;
38
+ args: Record<string, unknown>,
39
+ ) => Promise<unknown>
34
40
 
35
- type McpCapableContext = ToolContext & { callMcpTool: CallMcpTool };
41
+ type McpCapableContext = ToolContext & { callMcpTool: CallMcpTool }
36
42
 
37
43
  function hasMcpCapability(ctx: ToolContext): ctx is McpCapableContext {
38
- return "callMcpTool" in ctx;
44
+ return "callMcpTool" in ctx
39
45
  }
40
46
 
41
47
  function parseFinding(raw: unknown): SoloditFinding {
@@ -47,85 +53,254 @@ function parseFinding(raw: unknown): SoloditFinding {
47
53
  protocol: "",
48
54
  url: "",
49
55
  remediation: "",
50
- };
56
+ }
51
57
  }
52
58
 
53
- const obj = raw as Record<string, unknown>;
59
+ const obj = raw as Record<string, unknown>
54
60
  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
- };
61
+ title: typeof obj.title === "string" ? obj.title : "",
62
+ severity: typeof obj.severity === "string" ? obj.severity : "",
63
+ description: typeof obj.description === "string" ? obj.description : "",
64
+ protocol: typeof obj.protocol === "string" ? obj.protocol : "",
65
+ url: typeof obj.url === "string" ? obj.url : "",
66
+ remediation: typeof obj.remediation === "string" ? obj.remediation : "",
67
+ }
62
68
  }
63
69
 
64
70
  function parseFindings(response: unknown): SoloditFinding[] {
65
71
  if (!Array.isArray(response)) {
66
- return [];
72
+ return []
73
+ }
74
+ return response.map(parseFinding)
75
+ }
76
+
77
+ function parseFindingsFromAnyResponse(response: unknown): SoloditFinding[] {
78
+ const direct = parseFindings(response)
79
+ if (direct.length > 0) return direct
80
+
81
+ if (typeof response === "object" && response !== null) {
82
+ const findings = (response as Record<string, unknown>).findings
83
+ if (Array.isArray(findings)) return findings.map(parseFinding)
84
+ }
85
+
86
+ return extractFindingsFromMcpResponse(response)
87
+ }
88
+
89
+ function hasMcpError(response: unknown): boolean {
90
+ if (typeof response !== "object" || response === null) return false
91
+ const obj = response as Record<string, unknown>
92
+ return "error" in obj
93
+ }
94
+
95
+ function normalizeImpacts(
96
+ severity?: string[],
97
+ ): Array<"HIGH" | "MEDIUM" | "LOW" | "GAS"> | undefined {
98
+ if (!severity || severity.length === 0) return undefined
99
+ const allowed = new Set(["HIGH", "MEDIUM", "LOW", "GAS"] as const)
100
+ const impacts = severity
101
+ .map((s) => s.toUpperCase())
102
+ .filter((s): s is "HIGH" | "MEDIUM" | "LOW" | "GAS" =>
103
+ allowed.has(s as "HIGH" | "MEDIUM" | "LOW" | "GAS"),
104
+ )
105
+ return impacts.length > 0 ? impacts : undefined
106
+ }
107
+
108
+ function buildMcpArgs(
109
+ toolName: (typeof SOLODIT_MCP_TOOLS)[number],
110
+ query: string,
111
+ limit: number,
112
+ severity?: string[],
113
+ ): Record<string, unknown> {
114
+ if (toolName === "search") {
115
+ return { keywords: query }
116
+ }
117
+
118
+ const impact = normalizeImpacts(severity)
119
+ return {
120
+ keywords: query,
121
+ ...(impact ? { impact } : {}),
122
+ pageSize: limit,
123
+ }
124
+ }
125
+
126
+ function filterFindingsBySeverity(
127
+ findings: SoloditFinding[],
128
+ severities?: string[],
129
+ ): SoloditFinding[] {
130
+ if (!severities || severities.length === 0) return findings
131
+
132
+ const allowed = new Set(severities.map((s) => s.toLowerCase()))
133
+ return findings.filter((finding) => allowed.has(finding.severity.toLowerCase()))
134
+ }
135
+
136
+ function parseSseData(body: string): unknown {
137
+ for (const line of body.split("\n")) {
138
+ if (line.startsWith("data: ")) {
139
+ try {
140
+ return JSON.parse(line.slice(6))
141
+ } catch {}
142
+ }
143
+ }
144
+ try {
145
+ return JSON.parse(body)
146
+ } catch {
147
+ return null
67
148
  }
68
- return response.map(parseFinding);
149
+ }
150
+
151
+ function extractFindingsFromMcpResponse(envelope: unknown): SoloditFinding[] {
152
+ if (typeof envelope !== "object" || envelope === null) return []
153
+ const result = (envelope as Record<string, unknown>).result
154
+ if (typeof result !== "object" || result === null) return []
155
+
156
+ const structured = (result as Record<string, unknown>).structuredContent
157
+ const reportsJson =
158
+ typeof structured === "object" && structured !== null
159
+ ? (structured as Record<string, unknown>).reportsJSON
160
+ : undefined
161
+
162
+ if (typeof reportsJson === "string") {
163
+ try {
164
+ const parsed = JSON.parse(reportsJson)
165
+ if (Array.isArray(parsed)) return parsed.map(parseFinding)
166
+ } catch {
167
+ logger.debug("Failed to parse Solodit structured response")
168
+ }
169
+ }
170
+
171
+ const content = (result as Record<string, unknown>).content
172
+ if (Array.isArray(content) && content.length > 0) {
173
+ const first = content[0] as Record<string, unknown> | undefined
174
+ if (typeof first?.text === "string") {
175
+ try {
176
+ const parsed = JSON.parse(first.text)
177
+ if (Array.isArray(parsed)) return parsed.map(parseFinding)
178
+ } catch {
179
+ logger.debug("Failed to parse Solodit content text")
180
+ }
181
+ }
182
+ }
183
+
184
+ return []
185
+ }
186
+
187
+ async function callSoloditHttp(
188
+ query: string,
189
+ limit: number,
190
+ severities?: string[],
191
+ port: number = DEFAULT_SOLODIT_PORT,
192
+ ): Promise<SoloditSearchResult> {
193
+ let lastError: string | undefined
194
+
195
+ for (const toolName of SOLODIT_MCP_TOOLS) {
196
+ try {
197
+ const response = await fetch(`http://localhost:${port}/mcp`, {
198
+ method: "POST",
199
+ headers: {
200
+ "Content-Type": "application/json",
201
+ Accept: "application/json, text/event-stream",
202
+ },
203
+ body: JSON.stringify({
204
+ jsonrpc: "2.0",
205
+ method: "tools/call",
206
+ params: { name: toolName, arguments: buildMcpArgs(toolName, query, limit, severities) },
207
+ id: 1,
208
+ }),
209
+ signal: AbortSignal.timeout(SOLODIT_HTTP_TIMEOUT_MS),
210
+ })
211
+
212
+ if (!response.ok) {
213
+ lastError = `Solodit HTTP ${response.status}`
214
+ continue
215
+ }
216
+
217
+ const body = await response.text()
218
+ const envelope = parseSseData(body)
219
+
220
+ if (hasMcpError(envelope)) {
221
+ continue
222
+ }
223
+
224
+ const findings = filterFindingsBySeverity(parseFindingsFromAnyResponse(envelope), severities)
225
+
226
+ return { results: findings.slice(0, limit), totalFound: findings.length, query }
227
+ } catch (error) {
228
+ const message = error instanceof Error ? error.message : "Unknown error"
229
+ lastError = `Solodit MCP unreachable: ${message}`
230
+ }
231
+ }
232
+
233
+ return { results: [], totalFound: 0, query, error: lastError ?? "Solodit MCP call failed" }
69
234
  }
70
235
 
71
236
  export async function executeSoloditSearch(
72
237
  args: SoloditSearchArgs,
73
238
  context: ToolContext,
74
- callMcpTool?: CallMcpTool
239
+ callMcpTool?: CallMcpTool,
240
+ port: number = DEFAULT_SOLODIT_PORT,
75
241
  ): Promise<SoloditSearchResult> {
76
- const { query } = args;
77
- const limit = args.limit ?? DEFAULT_LIMIT;
242
+ const { query } = args
243
+ const limit = args.limit ?? DEFAULT_LIMIT
78
244
 
79
- context.metadata({ title: `Solodit search: ${query}` });
245
+ context.metadata({ title: `Solodit search: ${query}` })
80
246
 
81
- const mcpCaller =
82
- callMcpTool ?? (hasMcpCapability(context) ? context.callMcpTool : undefined);
247
+ const mcpCaller = callMcpTool ?? (hasMcpCapability(context) ? context.callMcpTool : undefined)
83
248
 
84
249
  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
- };
250
+ return callSoloditHttp(query, limit, args.severity, port)
91
251
  }
92
252
 
93
- try {
94
- const mcpArgs: Record<string, unknown> = { query, limit };
253
+ let hadMcpError = false
254
+ for (const toolName of SOLODIT_MCP_TOOLS) {
255
+ try {
256
+ const response = await mcpCaller(
257
+ SOLODIT_MCP_SERVER,
258
+ toolName,
259
+ buildMcpArgs(toolName, query, limit, args.severity),
260
+ )
95
261
 
96
- if (args.severity && args.severity.length > 0) {
97
- mcpArgs.filters = { severity: args.severity };
98
- }
262
+ if (hasMcpError(response)) {
263
+ hadMcpError = true
264
+ continue
265
+ }
99
266
 
100
- const response = await mcpCaller(SOLODIT_MCP_SERVER, SOLODIT_MCP_TOOL, mcpArgs);
101
- const findings = parseFindings(response);
267
+ const findings = filterFindingsBySeverity(
268
+ parseFindingsFromAnyResponse(response),
269
+ args.severity,
270
+ )
102
271
 
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
- };
272
+ return {
273
+ results: findings.slice(0, limit),
274
+ totalFound: findings.length,
275
+ query,
276
+ }
277
+ } catch {
278
+ hadMcpError = true
279
+ }
280
+ }
281
+
282
+ const fallback = await callSoloditHttp(query, limit, args.severity, port)
283
+ if (fallback.error || hadMcpError) {
284
+ return fallback
116
285
  }
286
+
287
+ return fallback
288
+ }
289
+
290
+ export function createSoloditSearchTool(port: number = DEFAULT_SOLODIT_PORT): ToolDefinition {
291
+ return tool({
292
+ description:
293
+ "Search Solodit audit findings database for known vulnerabilities and past audit results via the Solodit MCP server.",
294
+ args: {
295
+ query: tool.schema.string(),
296
+ severity: tool.schema.array(tool.schema.string()).optional(),
297
+ limit: tool.schema.number().optional(),
298
+ },
299
+ async execute(args, context) {
300
+ const result = await executeSoloditSearch(args, context, undefined, port)
301
+ return JSON.stringify(result)
302
+ },
303
+ })
117
304
  }
118
305
 
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
- });
306
+ export const soloditSearchTool = createSoloditSearchTool()
@@ -1,10 +1,11 @@
1
1
  import os from "node:os"
2
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"
3
+ import { type ToolContext, tool } from "@opencode-ai/plugin"
6
4
  import { loadArgusConfig } from "../config/loader"
7
5
  import type { ArgusConfig } from "../config/types"
6
+ import { ScvdClient } from "../knowledge/scvd-client"
7
+ import { type SyncResult, syncAll, syncIncremental } from "../knowledge/scvd-sync"
8
+ import { resolveProjectDir } from "../shared/project-utils"
8
9
 
9
10
  type SyncKnowledgeArgs = {
10
11
  force?: boolean
@@ -62,14 +63,14 @@ function toErrorMessage(error: unknown): string {
62
63
  export async function executeSyncKnowledge(
63
64
  args: SyncKnowledgeArgs,
64
65
  context: ToolContext,
65
- deps: SyncKnowledgeDependencies = {}
66
+ deps: SyncKnowledgeDependencies = {},
66
67
  ): Promise<SyncKnowledgeResult> {
67
68
  const dependencies = { ...defaultDependencies(), ...deps }
68
69
 
69
70
  context.metadata({ title: "Syncing SCVD knowledge index..." })
70
71
 
71
72
  try {
72
- const projectDir = context.directory ?? context.worktree ?? process.cwd()
73
+ const projectDir = resolveProjectDir(context)
73
74
  const argusConfig = dependencies.loadConfig(projectDir)
74
75
 
75
76
  if (!argusConfig.knowledge?.scvd?.enabled) {
@@ -81,12 +82,7 @@ export async function executeSyncKnowledge(
81
82
  }
82
83
 
83
84
  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
- )
85
+ const indexPath = path.join(os.homedir(), ".cache", "solidity-argus", "scvd-index.json")
90
86
 
91
87
  const client = dependencies.createClient(apiUrl, context.abort)
92
88
  const result = args.force
@@ -0,0 +1,118 @@
1
+ import { existsSync, readdirSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { createLogger } from "../shared/logger"
4
+
5
+ const logger = createLogger()
6
+
7
+ export interface AuditArtifact {
8
+ type: "audit-report" | "slither-output" | "deployment-artifact" | "security-tool-output"
9
+ path: string
10
+ name: string
11
+ }
12
+
13
+ /**
14
+ * Detects audit artifacts in a project directory (shallow scan, top-level only)
15
+ * @param projectDir Directory to scan for audit artifacts
16
+ * @returns Array of detected audit artifacts
17
+ */
18
+ export function detectAuditArtifacts(projectDir: string): AuditArtifact[] {
19
+ const artifacts: AuditArtifact[] = []
20
+
21
+ if (!existsSync(projectDir)) {
22
+ return artifacts
23
+ }
24
+
25
+ try {
26
+ const entries = readdirSync(projectDir, { withFileTypes: true })
27
+
28
+ for (const entry of entries) {
29
+ const fullPath = join(projectDir, entry.name)
30
+
31
+ // Check directories
32
+ if (entry.isDirectory()) {
33
+ // Audit report directories
34
+ if (["audit", "audits", "security"].includes(entry.name)) {
35
+ artifacts.push({
36
+ type: "audit-report",
37
+ path: fullPath,
38
+ name: entry.name,
39
+ })
40
+ continue
41
+ }
42
+
43
+ // Deployment artifact directories
44
+ if (entry.name === ".openzeppelin") {
45
+ artifacts.push({
46
+ type: "deployment-artifact",
47
+ path: fullPath,
48
+ name: entry.name,
49
+ })
50
+ continue
51
+ }
52
+
53
+ // docs/audit* directories
54
+ if (entry.name === "docs") {
55
+ try {
56
+ const docsEntries = readdirSync(fullPath, { withFileTypes: true })
57
+ for (const docsEntry of docsEntries) {
58
+ if (docsEntry.isDirectory() && docsEntry.name.startsWith("audit")) {
59
+ artifacts.push({
60
+ type: "audit-report",
61
+ path: join(fullPath, docsEntry.name),
62
+ name: docsEntry.name,
63
+ })
64
+ }
65
+ }
66
+ } catch {
67
+ logger.debug("Failed to read docs directory for audit artifacts")
68
+ }
69
+ }
70
+ continue
71
+ }
72
+
73
+ // Check files
74
+ if (entry.isFile()) {
75
+ // Audit report files
76
+ if (
77
+ /^.*audit.*\.(md|pdf)$/i.test(entry.name) ||
78
+ /^.*security-review.*\.(md|pdf)$/i.test(entry.name)
79
+ ) {
80
+ artifacts.push({
81
+ type: "audit-report",
82
+ path: fullPath,
83
+ name: entry.name,
84
+ })
85
+ continue
86
+ }
87
+
88
+ // Slither output files
89
+ if (
90
+ entry.name === "slither.json" ||
91
+ entry.name === "slither.sarif" ||
92
+ /^slither-report.*/.test(entry.name)
93
+ ) {
94
+ artifacts.push({
95
+ type: "slither-output",
96
+ path: fullPath,
97
+ name: entry.name,
98
+ })
99
+ continue
100
+ }
101
+
102
+ // Security tool output files
103
+ if (/^mythril-report.*/.test(entry.name) || /^securify-report.*/.test(entry.name)) {
104
+ artifacts.push({
105
+ type: "security-tool-output",
106
+ path: fullPath,
107
+ name: entry.name,
108
+ })
109
+ }
110
+ }
111
+ }
112
+ } catch {
113
+ // Return empty array if directory cannot be read
114
+ return []
115
+ }
116
+
117
+ return artifacts
118
+ }
@@ -0,0 +1,93 @@
1
+ export interface DependencyRisk {
2
+ package: string
3
+ version: string
4
+ risk: "high" | "medium" | "low"
5
+ category: string
6
+ recommendation: string
7
+ }
8
+
9
+ interface DependencyInput {
10
+ dependencies?: Record<string, string>
11
+ devDependencies?: Record<string, string>
12
+ }
13
+
14
+ function parseVersion(raw: string): [number, number, number] {
15
+ const cleaned = raw.replace(/^[^0-9]*/, "")
16
+ if (!cleaned) {
17
+ return [0, 0, 0]
18
+ }
19
+ const parts = cleaned.split(".")
20
+ const major = parseInt(parts[0] ?? "0", 10)
21
+ const minor = parseInt(parts[1] ?? "0", 10)
22
+ const patch = parseInt(parts[2] ?? "0", 10)
23
+ return [
24
+ Number.isNaN(major) ? 0 : major,
25
+ Number.isNaN(minor) ? 0 : minor,
26
+ Number.isNaN(patch) ? 0 : patch,
27
+ ]
28
+ }
29
+
30
+ function versionLt(raw: string, major: number, minor: number, patch = 0): boolean {
31
+ const [a, b, c] = parseVersion(raw)
32
+ if (a !== major) return a < major
33
+ if (b !== minor) return b < minor
34
+ return c < patch
35
+ }
36
+
37
+ export function scanDependencyRisks(input: DependencyInput): DependencyRisk[] {
38
+ const risks: DependencyRisk[] = []
39
+ const deps = input.dependencies ?? {}
40
+ const devDeps = input.devDependencies ?? {}
41
+ const allDeps = { ...deps, ...devDeps }
42
+
43
+ const ozVersion = deps["@openzeppelin/contracts"]
44
+ if (ozVersion) {
45
+ if (versionLt(ozVersion, 4, 9)) {
46
+ risks.push({
47
+ package: "@openzeppelin/contracts",
48
+ version: ozVersion,
49
+ risk: "high",
50
+ category: "known-vulnerability",
51
+ recommendation:
52
+ "Upgrade to @openzeppelin/contracts >= 4.9.0 — known vulnerabilities in OZ < 4.9",
53
+ })
54
+ } else if (versionLt(ozVersion, 5, 0)) {
55
+ risks.push({
56
+ package: "@openzeppelin/contracts",
57
+ version: ozVersion,
58
+ risk: "low",
59
+ category: "upgrade-available",
60
+ recommendation:
61
+ "Consider upgrading to OZ v5 for latest patterns and Solidity 0.8.20+ support",
62
+ })
63
+ }
64
+ }
65
+
66
+ const ozUpgradeableVersion = deps["@openzeppelin/contracts-upgradeable"]
67
+ if (ozUpgradeableVersion) {
68
+ const hasUpgradeTooling = "@openzeppelin/hardhat-upgrades" in allDeps
69
+ if (!hasUpgradeTooling) {
70
+ risks.push({
71
+ package: "@openzeppelin/contracts-upgradeable",
72
+ version: ozUpgradeableVersion,
73
+ risk: "medium",
74
+ category: "missing-tooling",
75
+ recommendation:
76
+ "Add @openzeppelin/hardhat-upgrades to devDependencies for safe upgrade workflows",
77
+ })
78
+ }
79
+ }
80
+
81
+ const solmateVersion = deps.solmate
82
+ if (solmateVersion && versionLt(solmateVersion, 6, 0)) {
83
+ risks.push({
84
+ package: "solmate",
85
+ version: solmateVersion,
86
+ risk: "medium",
87
+ category: "outdated",
88
+ recommendation: "Upgrade solmate to >= 6.0.0 for latest fixes",
89
+ })
90
+ }
91
+
92
+ return risks
93
+ }