solidity-argus 0.2.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -3
- package/README.md +93 -37
- package/package.json +34 -7
- package/skills/INVENTORY.md +88 -57
- package/skills/README.md +26 -23
- package/skills/case-studies/beanstalk-governance/SKILL.md +52 -0
- package/skills/case-studies/bzx-flash-loan/SKILL.md +53 -0
- package/skills/case-studies/cream-finance/SKILL.md +52 -0
- package/skills/case-studies/curve-reentrancy/SKILL.md +52 -0
- package/skills/case-studies/dao-hack/SKILL.md +51 -0
- package/skills/case-studies/euler-finance/SKILL.md +52 -0
- package/skills/case-studies/harvest-finance/SKILL.md +52 -0
- package/skills/case-studies/level-finance/SKILL.md +51 -0
- package/skills/case-studies/mango-markets/SKILL.md +53 -0
- package/skills/case-studies/nomad-bridge/SKILL.md +51 -0
- package/skills/case-studies/parity-multisig/SKILL.md +55 -0
- package/skills/case-studies/poly-network/SKILL.md +51 -0
- package/skills/case-studies/rari-fuse/SKILL.md +51 -0
- package/skills/case-studies/ronin-bridge/SKILL.md +52 -0
- package/skills/case-studies/wormhole-bridge/SKILL.md +51 -0
- package/skills/manifests/smartbugs.json +1 -3
- package/skills/manifests/sunweb3sec.json +1 -3
- package/skills/vulnerability-patterns/access-control/SKILL.md +14 -0
- package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
- package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
- package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
- package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +2 -1
- package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
- package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
- package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +2 -1
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +1 -0
- package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
- package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +1 -0
- package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
- package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
- package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
- package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
- package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
- package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
- package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
- package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
- package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
- package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
- package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
- package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
- package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
- package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
- package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +9 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +1 -0
- package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +9 -0
- package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
- package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
- package/skills/vulnerability-patterns/signature-malleability/SKILL.md +2 -1
- package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
- package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +2 -1
- package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
- package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
- package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
- package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
- package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
- package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
- package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
- package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
- package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
- package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
- package/src/agents/argus-prompt.ts +34 -7
- package/src/agents/pythia-prompt.ts +13 -4
- package/src/agents/scribe-prompt.ts +20 -2
- package/src/agents/sentinel-prompt.ts +45 -5
- package/src/cli/cli-program.ts +29 -26
- package/src/cli/commands/check-skills.ts +135 -0
- package/src/cli/commands/doctor.ts +48 -26
- package/src/cli/commands/init.ts +5 -3
- package/src/cli/commands/install.ts +7 -5
- package/src/cli/commands/lint-skills.ts +16 -12
- package/src/cli/index.ts +5 -5
- package/src/cli/types.ts +3 -3
- package/src/config/index.ts +1 -1
- package/src/config/loader.ts +4 -6
- package/src/config/schema.ts +6 -5
- package/src/config/types.ts +2 -2
- package/src/constants/defaults.ts +2 -0
- package/src/create-hooks.ts +145 -34
- package/src/create-managers.ts +10 -8
- package/src/create-tools.ts +13 -9
- package/src/features/background-agent/background-manager.ts +93 -87
- package/src/features/background-agent/index.ts +1 -1
- package/src/features/context-monitor/context-monitor.ts +3 -3
- package/src/features/context-monitor/index.ts +2 -2
- package/src/features/error-recovery/session-recovery.ts +2 -4
- package/src/features/error-recovery/tool-error-recovery.ts +12 -7
- package/src/features/index.ts +5 -5
- package/src/features/persistent-state/audit-state-manager.ts +143 -60
- package/src/features/persistent-state/global-run-index.ts +38 -0
- package/src/features/persistent-state/index.ts +1 -1
- package/src/features/persistent-state/run-journal.ts +86 -0
- package/src/hooks/config-handler.ts +28 -11
- package/src/hooks/context-budget.ts +2 -5
- package/src/hooks/event-hook.ts +47 -23
- package/src/hooks/hook-system.ts +4 -4
- package/src/hooks/index.ts +5 -5
- package/src/hooks/knowledge-sync-hook.ts +18 -21
- package/src/hooks/recon-context-builder.ts +2 -2
- package/src/hooks/safe-create-hook.ts +6 -7
- package/src/hooks/system-prompt-hook.ts +18 -1
- package/src/hooks/tool-tracking-hook.ts +110 -51
- package/src/hooks/types.ts +2 -1
- package/src/index.ts +24 -37
- package/src/knowledge/retry.ts +22 -22
- package/src/knowledge/scvd-client.ts +88 -95
- package/src/knowledge/scvd-errors.ts +35 -35
- package/src/knowledge/scvd-index.ts +78 -80
- package/src/knowledge/scvd-sync.ts +106 -101
- package/src/managers/index.ts +1 -1
- package/src/managers/types.ts +19 -14
- package/src/plugin-interface.ts +7 -9
- package/src/shared/binary-utils.ts +44 -35
- package/src/shared/deep-merge.ts +55 -36
- package/src/shared/file-utils.ts +21 -19
- package/src/shared/index.ts +11 -5
- package/src/shared/jsonc-parser.ts +123 -28
- package/src/shared/logger.ts +16 -3
- package/src/shared/project-utils.ts +30 -0
- package/src/skills/analysis/cluster.ts +414 -0
- package/src/skills/analysis/gates.ts +227 -0
- package/src/skills/analysis/index.ts +33 -0
- package/src/skills/analysis/normalize.ts +217 -0
- package/src/skills/analysis/similarity.ts +224 -0
- package/src/skills/argus-skill-resolver.ts +17 -6
- package/src/skills/skill-schema.ts +11 -10
- package/src/solodit-lifecycle.ts +203 -0
- package/src/state/audit-state.ts +8 -8
- package/src/state/finding-store.ts +68 -55
- package/src/state/types.ts +88 -67
- package/src/tools/argus-skill-load-tool.ts +12 -7
- package/src/tools/contract-analyzer-tool.ts +142 -77
- package/src/tools/forge-coverage-tool.ts +226 -0
- package/src/tools/forge-fuzz-tool.ts +127 -127
- package/src/tools/forge-test-tool.ts +201 -158
- package/src/tools/gas-analysis-tool.ts +264 -0
- package/src/tools/pattern-checker-tool.ts +203 -191
- package/src/tools/pattern-loader.ts +5 -111
- package/src/tools/pattern-schema.ts +3 -0
- package/src/tools/proxy-detection-tool.ts +224 -0
- package/src/tools/report-generator-tool.ts +305 -206
- package/src/tools/slither-tool.ts +266 -218
- package/src/tools/solodit-search-tool.ts +235 -119
- package/src/tools/sync-knowledge-tool.ts +7 -11
- package/src/utils/audit-artifact-detector.ts +28 -29
- package/src/utils/dependency-scanner.ts +37 -37
- package/src/utils/project-detector.ts +111 -124
- package/src/utils/solidity-parser.ts +175 -75
- package/skills/patterns/access-control.yaml +0 -31
- package/skills/patterns/erc4626.yaml +0 -29
- package/skills/patterns/flash-loan.yaml +0 -20
- package/skills/patterns/oracle.yaml +0 -30
- package/skills/patterns/proxy.yaml +0 -30
- package/skills/patterns/reentrancy.yaml +0 -30
- package/skills/patterns/signature.yaml +0 -31
- package/src/hooks/event-hook-v2.ts +0 -99
- package/src/state/plugin-state.ts +0 -14
|
@@ -1,43 +1,48 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ToolDefinition } from "@opencode-ai/plugin"
|
|
2
|
+
import { type ToolContext, tool } from "@opencode-ai/plugin"
|
|
3
|
+
import { createLogger } from "../shared/logger"
|
|
4
|
+
import { soloditAvailable } from "../solodit-lifecycle"
|
|
2
5
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
6
|
+
const logger = createLogger()
|
|
7
|
+
|
|
8
|
+
const SOLODIT_MCP_SERVER = "solodit-mcp"
|
|
9
|
+
const SOLODIT_MCP_TOOLS = ["search", "search_findings"] as const
|
|
10
|
+
const DEFAULT_LIMIT = 10
|
|
11
|
+
const DEFAULT_SOLODIT_PORT = 3000
|
|
12
|
+
const SOLODIT_HTTP_TIMEOUT_MS = 10_000
|
|
8
13
|
|
|
9
14
|
type SoloditSearchArgs = {
|
|
10
|
-
query: string
|
|
11
|
-
severity?: string[]
|
|
12
|
-
limit?: number
|
|
13
|
-
}
|
|
15
|
+
query: string
|
|
16
|
+
severity?: string[]
|
|
17
|
+
limit?: number
|
|
18
|
+
}
|
|
14
19
|
|
|
15
20
|
type SoloditFinding = {
|
|
16
|
-
title: string
|
|
17
|
-
severity: string
|
|
18
|
-
description: string
|
|
19
|
-
protocol: string
|
|
20
|
-
url: string
|
|
21
|
-
remediation: string
|
|
22
|
-
}
|
|
21
|
+
title: string
|
|
22
|
+
severity: string
|
|
23
|
+
description: string
|
|
24
|
+
protocol: string
|
|
25
|
+
url: string
|
|
26
|
+
remediation: string
|
|
27
|
+
}
|
|
23
28
|
|
|
24
29
|
export type SoloditSearchResult = {
|
|
25
|
-
results: SoloditFinding[]
|
|
26
|
-
totalFound: number
|
|
27
|
-
query: string
|
|
28
|
-
error?: string
|
|
29
|
-
}
|
|
30
|
+
results: SoloditFinding[]
|
|
31
|
+
totalFound: number
|
|
32
|
+
query: string
|
|
33
|
+
error?: string
|
|
34
|
+
}
|
|
30
35
|
|
|
31
36
|
export type CallMcpTool = (
|
|
32
37
|
server: string,
|
|
33
38
|
tool: string,
|
|
34
|
-
args: Record<string, unknown
|
|
35
|
-
) => Promise<unknown
|
|
39
|
+
args: Record<string, unknown>,
|
|
40
|
+
) => Promise<unknown>
|
|
36
41
|
|
|
37
|
-
type McpCapableContext = ToolContext & { callMcpTool: CallMcpTool }
|
|
42
|
+
type McpCapableContext = ToolContext & { callMcpTool: CallMcpTool }
|
|
38
43
|
|
|
39
44
|
function hasMcpCapability(ctx: ToolContext): ctx is McpCapableContext {
|
|
40
|
-
return "callMcpTool" in ctx
|
|
45
|
+
return "callMcpTool" in ctx
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
function parseFinding(raw: unknown): SoloditFinding {
|
|
@@ -49,161 +54,272 @@ function parseFinding(raw: unknown): SoloditFinding {
|
|
|
49
54
|
protocol: "",
|
|
50
55
|
url: "",
|
|
51
56
|
remediation: "",
|
|
52
|
-
}
|
|
57
|
+
}
|
|
53
58
|
}
|
|
54
59
|
|
|
55
|
-
const obj = raw as Record<string, unknown
|
|
60
|
+
const obj = raw as Record<string, unknown>
|
|
56
61
|
return {
|
|
57
|
-
title: typeof obj
|
|
58
|
-
severity: typeof obj
|
|
59
|
-
description: typeof obj
|
|
60
|
-
protocol: typeof obj
|
|
61
|
-
url: typeof obj
|
|
62
|
-
remediation: typeof obj
|
|
63
|
-
}
|
|
62
|
+
title: typeof obj.title === "string" ? obj.title : "",
|
|
63
|
+
severity: typeof obj.severity === "string" ? obj.severity : "",
|
|
64
|
+
description: typeof obj.description === "string" ? obj.description : "",
|
|
65
|
+
protocol: typeof obj.protocol === "string" ? obj.protocol : "",
|
|
66
|
+
url: typeof obj.url === "string" ? obj.url : "",
|
|
67
|
+
remediation: typeof obj.remediation === "string" ? obj.remediation : "",
|
|
68
|
+
}
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
function parseFindings(response: unknown): SoloditFinding[] {
|
|
67
72
|
if (!Array.isArray(response)) {
|
|
68
|
-
return []
|
|
73
|
+
return []
|
|
74
|
+
}
|
|
75
|
+
return response.map(parseFinding)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseFindingsFromAnyResponse(response: unknown): SoloditFinding[] {
|
|
79
|
+
const direct = parseFindings(response)
|
|
80
|
+
if (direct.length > 0) return direct
|
|
81
|
+
|
|
82
|
+
if (typeof response === "object" && response !== null) {
|
|
83
|
+
const findings = (response as Record<string, unknown>).findings
|
|
84
|
+
if (Array.isArray(findings)) return findings.map(parseFinding)
|
|
69
85
|
}
|
|
70
|
-
|
|
86
|
+
|
|
87
|
+
return extractFindingsFromMcpResponse(response)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function hasMcpError(response: unknown): boolean {
|
|
91
|
+
if (typeof response !== "object" || response === null) return false
|
|
92
|
+
const obj = response as Record<string, unknown>
|
|
93
|
+
return "error" in obj
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeImpacts(
|
|
97
|
+
severity?: string[],
|
|
98
|
+
): Array<"HIGH" | "MEDIUM" | "LOW" | "GAS"> | undefined {
|
|
99
|
+
if (!severity || severity.length === 0) return undefined
|
|
100
|
+
const allowed = new Set(["HIGH", "MEDIUM", "LOW", "GAS"] as const)
|
|
101
|
+
const impacts = severity
|
|
102
|
+
.map((s) => s.toUpperCase())
|
|
103
|
+
.filter((s): s is "HIGH" | "MEDIUM" | "LOW" | "GAS" =>
|
|
104
|
+
allowed.has(s as "HIGH" | "MEDIUM" | "LOW" | "GAS"),
|
|
105
|
+
)
|
|
106
|
+
return impacts.length > 0 ? impacts : undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildMcpArgs(
|
|
110
|
+
toolName: (typeof SOLODIT_MCP_TOOLS)[number],
|
|
111
|
+
query: string,
|
|
112
|
+
limit: number,
|
|
113
|
+
severity?: string[],
|
|
114
|
+
): Record<string, unknown> {
|
|
115
|
+
if (toolName === "search") {
|
|
116
|
+
return { keywords: query }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const impact = normalizeImpacts(severity)
|
|
120
|
+
return {
|
|
121
|
+
keywords: query,
|
|
122
|
+
...(impact ? { impact } : {}),
|
|
123
|
+
pageSize: limit,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function filterFindingsBySeverity(
|
|
128
|
+
findings: SoloditFinding[],
|
|
129
|
+
severities?: string[],
|
|
130
|
+
): SoloditFinding[] {
|
|
131
|
+
if (!severities || severities.length === 0) return findings
|
|
132
|
+
|
|
133
|
+
const allowed = new Set(severities.map((s) => s.toLowerCase()))
|
|
134
|
+
return findings.filter((finding) => allowed.has(finding.severity.toLowerCase()))
|
|
71
135
|
}
|
|
72
136
|
|
|
73
137
|
function parseSseData(body: string): unknown {
|
|
74
138
|
for (const line of body.split("\n")) {
|
|
75
139
|
if (line.startsWith("data: ")) {
|
|
76
140
|
try {
|
|
77
|
-
return JSON.parse(line.slice(6))
|
|
78
|
-
} catch {
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
141
|
+
return JSON.parse(line.slice(6))
|
|
142
|
+
} catch {}
|
|
81
143
|
}
|
|
82
144
|
}
|
|
83
145
|
try {
|
|
84
|
-
return JSON.parse(body)
|
|
146
|
+
return JSON.parse(body)
|
|
85
147
|
} catch {
|
|
86
|
-
return null
|
|
148
|
+
return null
|
|
87
149
|
}
|
|
88
150
|
}
|
|
89
151
|
|
|
90
152
|
function extractFindingsFromMcpResponse(envelope: unknown): SoloditFinding[] {
|
|
91
|
-
if (typeof envelope !== "object" || envelope === null) return []
|
|
92
|
-
const result = (envelope as Record<string, unknown>).result
|
|
93
|
-
if (typeof result !== "object" || result === null) return []
|
|
153
|
+
if (typeof envelope !== "object" || envelope === null) return []
|
|
154
|
+
const result = (envelope as Record<string, unknown>).result
|
|
155
|
+
if (typeof result !== "object" || result === null) return []
|
|
94
156
|
|
|
95
|
-
const structured = (result as Record<string, unknown>).structuredContent
|
|
157
|
+
const structured = (result as Record<string, unknown>).structuredContent
|
|
96
158
|
const reportsJson =
|
|
97
159
|
typeof structured === "object" && structured !== null
|
|
98
160
|
? (structured as Record<string, unknown>).reportsJSON
|
|
99
|
-
: undefined
|
|
161
|
+
: undefined
|
|
100
162
|
|
|
101
163
|
if (typeof reportsJson === "string") {
|
|
102
164
|
try {
|
|
103
|
-
const parsed = JSON.parse(reportsJson)
|
|
104
|
-
if (Array.isArray(parsed)) return parsed.map(parseFinding)
|
|
105
|
-
} catch {
|
|
165
|
+
const parsed = JSON.parse(reportsJson)
|
|
166
|
+
if (Array.isArray(parsed)) return parsed.map(parseFinding)
|
|
167
|
+
} catch {
|
|
168
|
+
logger.debug("Failed to parse Solodit structured response")
|
|
169
|
+
}
|
|
106
170
|
}
|
|
107
171
|
|
|
108
|
-
const content = (result as Record<string, unknown>).content
|
|
172
|
+
const content = (result as Record<string, unknown>).content
|
|
109
173
|
if (Array.isArray(content) && content.length > 0) {
|
|
110
|
-
const first = content[0] as Record<string, unknown> | undefined
|
|
174
|
+
const first = content[0] as Record<string, unknown> | undefined
|
|
111
175
|
if (typeof first?.text === "string") {
|
|
112
176
|
try {
|
|
113
|
-
const parsed = JSON.parse(first.text)
|
|
114
|
-
if (Array.isArray(parsed)) return parsed.map(parseFinding)
|
|
115
|
-
} catch {
|
|
177
|
+
const parsed = JSON.parse(first.text)
|
|
178
|
+
if (Array.isArray(parsed)) return parsed.map(parseFinding)
|
|
179
|
+
} catch {
|
|
180
|
+
logger.debug("Failed to parse Solodit content text")
|
|
181
|
+
}
|
|
116
182
|
}
|
|
117
183
|
}
|
|
118
184
|
|
|
119
|
-
return []
|
|
185
|
+
return []
|
|
120
186
|
}
|
|
121
187
|
|
|
122
188
|
async function callSoloditHttp(
|
|
123
189
|
query: string,
|
|
124
190
|
limit: number,
|
|
191
|
+
severities?: string[],
|
|
125
192
|
port: number = DEFAULT_SOLODIT_PORT,
|
|
126
193
|
): Promise<SoloditSearchResult> {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
194
|
+
let lastError: string | undefined
|
|
195
|
+
|
|
196
|
+
for (const toolName of SOLODIT_MCP_TOOLS) {
|
|
197
|
+
try {
|
|
198
|
+
const response = await fetch(`http://localhost:${port}/mcp`, {
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: {
|
|
201
|
+
"Content-Type": "application/json",
|
|
202
|
+
Accept: "application/json, text/event-stream",
|
|
203
|
+
},
|
|
204
|
+
body: JSON.stringify({
|
|
205
|
+
jsonrpc: "2.0",
|
|
206
|
+
method: "tools/call",
|
|
207
|
+
params: { name: toolName, arguments: buildMcpArgs(toolName, query, limit, severities) },
|
|
208
|
+
id: 1,
|
|
209
|
+
}),
|
|
210
|
+
signal: AbortSignal.timeout(SOLODIT_HTTP_TIMEOUT_MS),
|
|
211
|
+
})
|
|
146
212
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
lastError = `Solodit HTTP ${response.status}`
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const body = await response.text()
|
|
219
|
+
const envelope = parseSseData(body)
|
|
220
|
+
|
|
221
|
+
if (hasMcpError(envelope)) {
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
150
224
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
225
|
+
const findings = filterFindingsBySeverity(parseFindingsFromAnyResponse(envelope), severities)
|
|
226
|
+
|
|
227
|
+
return { results: findings.slice(0, limit), totalFound: findings.length, query }
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const message = error instanceof Error ? error.message : "Unknown error"
|
|
230
|
+
lastError = `Solodit MCP unreachable: ${message}`
|
|
231
|
+
}
|
|
155
232
|
}
|
|
233
|
+
|
|
234
|
+
return { results: [], totalFound: 0, query, error: lastError ?? "Solodit MCP call failed" }
|
|
156
235
|
}
|
|
157
236
|
|
|
158
237
|
export async function executeSoloditSearch(
|
|
159
238
|
args: SoloditSearchArgs,
|
|
160
239
|
context: ToolContext,
|
|
161
|
-
callMcpTool?: CallMcpTool
|
|
240
|
+
callMcpTool?: CallMcpTool,
|
|
241
|
+
port: number = DEFAULT_SOLODIT_PORT,
|
|
162
242
|
): Promise<SoloditSearchResult> {
|
|
163
|
-
const { query } = args
|
|
164
|
-
const limit = args.limit ?? DEFAULT_LIMIT
|
|
243
|
+
const { query } = args
|
|
244
|
+
const limit = args.limit ?? DEFAULT_LIMIT
|
|
165
245
|
|
|
166
|
-
context.metadata({ title: `Solodit search: ${query}` })
|
|
246
|
+
context.metadata({ title: `Solodit search: ${query}` })
|
|
247
|
+
|
|
248
|
+
// Belt-and-suspenders: check if Solodit MCP is available, with 3s retry
|
|
249
|
+
// Skip check in test environment
|
|
250
|
+
if (!soloditAvailable && process.env.NODE_ENV !== "test") {
|
|
251
|
+
// Wait up to 3s for monitoring to flip the flag
|
|
252
|
+
for (let i = 0; i < 3 && !soloditAvailable; i++) {
|
|
253
|
+
await Bun.sleep(1000)
|
|
254
|
+
}
|
|
255
|
+
if (!soloditAvailable) {
|
|
256
|
+
return {
|
|
257
|
+
results: [],
|
|
258
|
+
totalFound: 0,
|
|
259
|
+
query,
|
|
260
|
+
error:
|
|
261
|
+
"Solodit MCP not available — server did not start. Results limited to local patterns.",
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
167
265
|
|
|
168
|
-
const mcpCaller =
|
|
169
|
-
callMcpTool ?? (hasMcpCapability(context) ? context.callMcpTool : undefined);
|
|
266
|
+
const mcpCaller = callMcpTool ?? (hasMcpCapability(context) ? context.callMcpTool : undefined)
|
|
170
267
|
|
|
171
268
|
if (!mcpCaller) {
|
|
172
|
-
return callSoloditHttp(query, limit)
|
|
269
|
+
return callSoloditHttp(query, limit, args.severity, port)
|
|
173
270
|
}
|
|
174
271
|
|
|
175
|
-
|
|
176
|
-
|
|
272
|
+
let hadMcpError = false
|
|
273
|
+
for (const toolName of SOLODIT_MCP_TOOLS) {
|
|
274
|
+
try {
|
|
275
|
+
const response = await mcpCaller(
|
|
276
|
+
SOLODIT_MCP_SERVER,
|
|
277
|
+
toolName,
|
|
278
|
+
buildMcpArgs(toolName, query, limit, args.severity),
|
|
279
|
+
)
|
|
177
280
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
281
|
+
if (hasMcpError(response)) {
|
|
282
|
+
hadMcpError = true
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
181
285
|
|
|
182
|
-
|
|
183
|
-
|
|
286
|
+
const findings = filterFindingsBySeverity(
|
|
287
|
+
parseFindingsFromAnyResponse(response),
|
|
288
|
+
args.severity,
|
|
289
|
+
)
|
|
184
290
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
291
|
+
return {
|
|
292
|
+
results: findings.slice(0, limit),
|
|
293
|
+
totalFound: findings.length,
|
|
294
|
+
query,
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
hadMcpError = true
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const fallback = await callSoloditHttp(query, limit, args.severity, port)
|
|
302
|
+
if (fallback.error || hadMcpError) {
|
|
303
|
+
return fallback
|
|
194
304
|
}
|
|
305
|
+
|
|
306
|
+
return fallback
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function createSoloditSearchTool(port: number = DEFAULT_SOLODIT_PORT): ToolDefinition {
|
|
310
|
+
return tool({
|
|
311
|
+
description:
|
|
312
|
+
"Search Solodit audit findings database for known vulnerabilities and past audit results via the Solodit MCP server.",
|
|
313
|
+
args: {
|
|
314
|
+
query: tool.schema.string(),
|
|
315
|
+
severity: tool.schema.array(tool.schema.string()).optional(),
|
|
316
|
+
limit: tool.schema.number().optional(),
|
|
317
|
+
},
|
|
318
|
+
async execute(args, context) {
|
|
319
|
+
const result = await executeSoloditSearch(args, context, undefined, port)
|
|
320
|
+
return JSON.stringify(result)
|
|
321
|
+
},
|
|
322
|
+
})
|
|
195
323
|
}
|
|
196
324
|
|
|
197
|
-
export const soloditSearchTool =
|
|
198
|
-
description:
|
|
199
|
-
"Search Solodit audit findings database for known vulnerabilities and past audit results via the Solodit MCP server.",
|
|
200
|
-
args: {
|
|
201
|
-
query: tool.schema.string(),
|
|
202
|
-
severity: tool.schema.array(tool.schema.string()).optional(),
|
|
203
|
-
limit: tool.schema.number().optional(),
|
|
204
|
-
},
|
|
205
|
-
async execute(args, context) {
|
|
206
|
-
const result = await executeSoloditSearch(args, context);
|
|
207
|
-
return JSON.stringify(result);
|
|
208
|
-
},
|
|
209
|
-
});
|
|
325
|
+
export const soloditSearchTool = createSoloditSearchTool()
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import os from "node:os"
|
|
2
2
|
import path from "node:path"
|
|
3
|
-
import {
|
|
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
|
|
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
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from "fs"
|
|
2
|
-
import { join } from "path"
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { createLogger } from "../shared/logger"
|
|
4
|
+
|
|
5
|
+
const logger = createLogger()
|
|
3
6
|
|
|
4
7
|
export interface AuditArtifact {
|
|
5
|
-
type: "audit-report" | "slither-output" | "deployment-artifact" | "security-tool-output"
|
|
6
|
-
path: string
|
|
7
|
-
name: string
|
|
8
|
+
type: "audit-report" | "slither-output" | "deployment-artifact" | "security-tool-output"
|
|
9
|
+
path: string
|
|
10
|
+
name: string
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
/**
|
|
@@ -13,17 +16,17 @@ export interface AuditArtifact {
|
|
|
13
16
|
* @returns Array of detected audit artifacts
|
|
14
17
|
*/
|
|
15
18
|
export function detectAuditArtifacts(projectDir: string): AuditArtifact[] {
|
|
16
|
-
const artifacts: AuditArtifact[] = []
|
|
19
|
+
const artifacts: AuditArtifact[] = []
|
|
17
20
|
|
|
18
21
|
if (!existsSync(projectDir)) {
|
|
19
|
-
return artifacts
|
|
22
|
+
return artifacts
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
try {
|
|
23
|
-
const entries = readdirSync(projectDir, { withFileTypes: true })
|
|
26
|
+
const entries = readdirSync(projectDir, { withFileTypes: true })
|
|
24
27
|
|
|
25
28
|
for (const entry of entries) {
|
|
26
|
-
const fullPath = join(projectDir, entry.name)
|
|
29
|
+
const fullPath = join(projectDir, entry.name)
|
|
27
30
|
|
|
28
31
|
// Check directories
|
|
29
32
|
if (entry.isDirectory()) {
|
|
@@ -33,8 +36,8 @@ export function detectAuditArtifacts(projectDir: string): AuditArtifact[] {
|
|
|
33
36
|
type: "audit-report",
|
|
34
37
|
path: fullPath,
|
|
35
38
|
name: entry.name,
|
|
36
|
-
})
|
|
37
|
-
continue
|
|
39
|
+
})
|
|
40
|
+
continue
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
// Deployment artifact directories
|
|
@@ -43,28 +46,28 @@ export function detectAuditArtifacts(projectDir: string): AuditArtifact[] {
|
|
|
43
46
|
type: "deployment-artifact",
|
|
44
47
|
path: fullPath,
|
|
45
48
|
name: entry.name,
|
|
46
|
-
})
|
|
47
|
-
continue
|
|
49
|
+
})
|
|
50
|
+
continue
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
// docs/audit* directories
|
|
51
54
|
if (entry.name === "docs") {
|
|
52
55
|
try {
|
|
53
|
-
const docsEntries = readdirSync(fullPath, { withFileTypes: true })
|
|
56
|
+
const docsEntries = readdirSync(fullPath, { withFileTypes: true })
|
|
54
57
|
for (const docsEntry of docsEntries) {
|
|
55
58
|
if (docsEntry.isDirectory() && docsEntry.name.startsWith("audit")) {
|
|
56
59
|
artifacts.push({
|
|
57
60
|
type: "audit-report",
|
|
58
61
|
path: join(fullPath, docsEntry.name),
|
|
59
62
|
name: docsEntry.name,
|
|
60
|
-
})
|
|
63
|
+
})
|
|
61
64
|
}
|
|
62
65
|
}
|
|
63
66
|
} catch {
|
|
64
|
-
|
|
67
|
+
logger.debug("Failed to read docs directory for audit artifacts")
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
|
-
continue
|
|
70
|
+
continue
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
// Check files
|
|
@@ -78,8 +81,8 @@ export function detectAuditArtifacts(projectDir: string): AuditArtifact[] {
|
|
|
78
81
|
type: "audit-report",
|
|
79
82
|
path: fullPath,
|
|
80
83
|
name: entry.name,
|
|
81
|
-
})
|
|
82
|
-
continue
|
|
84
|
+
})
|
|
85
|
+
continue
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
// Slither output files
|
|
@@ -92,28 +95,24 @@ export function detectAuditArtifacts(projectDir: string): AuditArtifact[] {
|
|
|
92
95
|
type: "slither-output",
|
|
93
96
|
path: fullPath,
|
|
94
97
|
name: entry.name,
|
|
95
|
-
})
|
|
96
|
-
continue
|
|
98
|
+
})
|
|
99
|
+
continue
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
// Security tool output files
|
|
100
|
-
if (
|
|
101
|
-
/^mythril-report.*/.test(entry.name) ||
|
|
102
|
-
/^securify-report.*/.test(entry.name)
|
|
103
|
-
) {
|
|
103
|
+
if (/^mythril-report.*/.test(entry.name) || /^securify-report.*/.test(entry.name)) {
|
|
104
104
|
artifacts.push({
|
|
105
105
|
type: "security-tool-output",
|
|
106
106
|
path: fullPath,
|
|
107
107
|
name: entry.name,
|
|
108
|
-
})
|
|
109
|
-
continue;
|
|
108
|
+
})
|
|
110
109
|
}
|
|
111
110
|
}
|
|
112
111
|
}
|
|
113
112
|
} catch {
|
|
114
113
|
// Return empty array if directory cannot be read
|
|
115
|
-
return []
|
|
114
|
+
return []
|
|
116
115
|
}
|
|
117
116
|
|
|
118
|
-
return artifacts
|
|
117
|
+
return artifacts
|
|
119
118
|
}
|