solidity-argus 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -3
- package/README.md +93 -37
- package/package.json +33 -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 +24 -7
- package/src/agents/pythia-prompt.ts +3 -4
- package/src/agents/scribe-prompt.ts +7 -2
- package/src/agents/sentinel-prompt.ts +32 -3
- 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 +4 -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/tool-tracking-hook.ts +104 -50
- package/src/hooks/types.ts +2 -1
- package/src/index.ts +23 -36
- 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 +202 -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 +60 -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 +153 -157
- package/src/tools/gas-analysis-tool.ts +264 -0
- package/src/tools/pattern-checker-tool.ts +185 -190
- package/src/tools/pattern-loader.ts +5 -111
- package/src/tools/proxy-detection-tool.ts +224 -0
- package/src/tools/report-generator-tool.ts +268 -200
- package/src/tools/slither-tool.ts +266 -218
- package/src/tools/solodit-search-tool.ts +216 -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 +103 -74
- 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,47 @@
|
|
|
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"
|
|
2
4
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
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
|
|
8
12
|
|
|
9
13
|
type SoloditSearchArgs = {
|
|
10
|
-
query: string
|
|
11
|
-
severity?: string[]
|
|
12
|
-
limit?: number
|
|
13
|
-
}
|
|
14
|
+
query: string
|
|
15
|
+
severity?: string[]
|
|
16
|
+
limit?: number
|
|
17
|
+
}
|
|
14
18
|
|
|
15
19
|
type SoloditFinding = {
|
|
16
|
-
title: string
|
|
17
|
-
severity: string
|
|
18
|
-
description: string
|
|
19
|
-
protocol: string
|
|
20
|
-
url: string
|
|
21
|
-
remediation: string
|
|
22
|
-
}
|
|
20
|
+
title: string
|
|
21
|
+
severity: string
|
|
22
|
+
description: string
|
|
23
|
+
protocol: string
|
|
24
|
+
url: string
|
|
25
|
+
remediation: string
|
|
26
|
+
}
|
|
23
27
|
|
|
24
28
|
export type SoloditSearchResult = {
|
|
25
|
-
results: SoloditFinding[]
|
|
26
|
-
totalFound: number
|
|
27
|
-
query: string
|
|
28
|
-
error?: string
|
|
29
|
-
}
|
|
29
|
+
results: SoloditFinding[]
|
|
30
|
+
totalFound: number
|
|
31
|
+
query: string
|
|
32
|
+
error?: string
|
|
33
|
+
}
|
|
30
34
|
|
|
31
35
|
export type CallMcpTool = (
|
|
32
36
|
server: string,
|
|
33
37
|
tool: string,
|
|
34
|
-
args: Record<string, unknown
|
|
35
|
-
) => Promise<unknown
|
|
38
|
+
args: Record<string, unknown>,
|
|
39
|
+
) => Promise<unknown>
|
|
36
40
|
|
|
37
|
-
type McpCapableContext = ToolContext & { callMcpTool: CallMcpTool }
|
|
41
|
+
type McpCapableContext = ToolContext & { callMcpTool: CallMcpTool }
|
|
38
42
|
|
|
39
43
|
function hasMcpCapability(ctx: ToolContext): ctx is McpCapableContext {
|
|
40
|
-
return "callMcpTool" in ctx
|
|
44
|
+
return "callMcpTool" in ctx
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
function parseFinding(raw: unknown): SoloditFinding {
|
|
@@ -49,161 +53,254 @@ function parseFinding(raw: unknown): SoloditFinding {
|
|
|
49
53
|
protocol: "",
|
|
50
54
|
url: "",
|
|
51
55
|
remediation: "",
|
|
52
|
-
}
|
|
56
|
+
}
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
const obj = raw as Record<string, unknown
|
|
59
|
+
const obj = raw as Record<string, unknown>
|
|
56
60
|
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
|
-
}
|
|
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
|
+
}
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
function parseFindings(response: unknown): SoloditFinding[] {
|
|
67
71
|
if (!Array.isArray(response)) {
|
|
68
|
-
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,
|
|
69
123
|
}
|
|
70
|
-
|
|
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()))
|
|
71
134
|
}
|
|
72
135
|
|
|
73
136
|
function parseSseData(body: string): unknown {
|
|
74
137
|
for (const line of body.split("\n")) {
|
|
75
138
|
if (line.startsWith("data: ")) {
|
|
76
139
|
try {
|
|
77
|
-
return JSON.parse(line.slice(6))
|
|
78
|
-
} catch {
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
140
|
+
return JSON.parse(line.slice(6))
|
|
141
|
+
} catch {}
|
|
81
142
|
}
|
|
82
143
|
}
|
|
83
144
|
try {
|
|
84
|
-
return JSON.parse(body)
|
|
145
|
+
return JSON.parse(body)
|
|
85
146
|
} catch {
|
|
86
|
-
return null
|
|
147
|
+
return null
|
|
87
148
|
}
|
|
88
149
|
}
|
|
89
150
|
|
|
90
151
|
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 []
|
|
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 []
|
|
94
155
|
|
|
95
|
-
const structured = (result as Record<string, unknown>).structuredContent
|
|
156
|
+
const structured = (result as Record<string, unknown>).structuredContent
|
|
96
157
|
const reportsJson =
|
|
97
158
|
typeof structured === "object" && structured !== null
|
|
98
159
|
? (structured as Record<string, unknown>).reportsJSON
|
|
99
|
-
: undefined
|
|
160
|
+
: undefined
|
|
100
161
|
|
|
101
162
|
if (typeof reportsJson === "string") {
|
|
102
163
|
try {
|
|
103
|
-
const parsed = JSON.parse(reportsJson)
|
|
104
|
-
if (Array.isArray(parsed)) return parsed.map(parseFinding)
|
|
105
|
-
} catch {
|
|
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
|
+
}
|
|
106
169
|
}
|
|
107
170
|
|
|
108
|
-
const content = (result as Record<string, unknown>).content
|
|
171
|
+
const content = (result as Record<string, unknown>).content
|
|
109
172
|
if (Array.isArray(content) && content.length > 0) {
|
|
110
|
-
const first = content[0] as Record<string, unknown> | undefined
|
|
173
|
+
const first = content[0] as Record<string, unknown> | undefined
|
|
111
174
|
if (typeof first?.text === "string") {
|
|
112
175
|
try {
|
|
113
|
-
const parsed = JSON.parse(first.text)
|
|
114
|
-
if (Array.isArray(parsed)) return parsed.map(parseFinding)
|
|
115
|
-
} catch {
|
|
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
|
+
}
|
|
116
181
|
}
|
|
117
182
|
}
|
|
118
183
|
|
|
119
|
-
return []
|
|
184
|
+
return []
|
|
120
185
|
}
|
|
121
186
|
|
|
122
187
|
async function callSoloditHttp(
|
|
123
188
|
query: string,
|
|
124
189
|
limit: number,
|
|
190
|
+
severities?: string[],
|
|
125
191
|
port: number = DEFAULT_SOLODIT_PORT,
|
|
126
192
|
): Promise<SoloditSearchResult> {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
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
|
+
})
|
|
146
211
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
+
}
|
|
150
223
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
}
|
|
155
231
|
}
|
|
232
|
+
|
|
233
|
+
return { results: [], totalFound: 0, query, error: lastError ?? "Solodit MCP call failed" }
|
|
156
234
|
}
|
|
157
235
|
|
|
158
236
|
export async function executeSoloditSearch(
|
|
159
237
|
args: SoloditSearchArgs,
|
|
160
238
|
context: ToolContext,
|
|
161
|
-
callMcpTool?: CallMcpTool
|
|
239
|
+
callMcpTool?: CallMcpTool,
|
|
240
|
+
port: number = DEFAULT_SOLODIT_PORT,
|
|
162
241
|
): Promise<SoloditSearchResult> {
|
|
163
|
-
const { query } = args
|
|
164
|
-
const limit = args.limit ?? DEFAULT_LIMIT
|
|
242
|
+
const { query } = args
|
|
243
|
+
const limit = args.limit ?? DEFAULT_LIMIT
|
|
165
244
|
|
|
166
|
-
context.metadata({ title: `Solodit search: ${query}` })
|
|
245
|
+
context.metadata({ title: `Solodit search: ${query}` })
|
|
167
246
|
|
|
168
|
-
const mcpCaller =
|
|
169
|
-
callMcpTool ?? (hasMcpCapability(context) ? context.callMcpTool : undefined);
|
|
247
|
+
const mcpCaller = callMcpTool ?? (hasMcpCapability(context) ? context.callMcpTool : undefined)
|
|
170
248
|
|
|
171
249
|
if (!mcpCaller) {
|
|
172
|
-
return callSoloditHttp(query, limit)
|
|
250
|
+
return callSoloditHttp(query, limit, args.severity, port)
|
|
173
251
|
}
|
|
174
252
|
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
)
|
|
177
261
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
262
|
+
if (hasMcpError(response)) {
|
|
263
|
+
hadMcpError = true
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
181
266
|
|
|
182
|
-
|
|
183
|
-
|
|
267
|
+
const findings = filterFindingsBySeverity(
|
|
268
|
+
parseFindingsFromAnyResponse(response),
|
|
269
|
+
args.severity,
|
|
270
|
+
)
|
|
184
271
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
194
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
|
+
})
|
|
195
304
|
}
|
|
196
305
|
|
|
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
|
-
});
|
|
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 {
|
|
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
|
}
|