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.
- package/AGENTS.md +3 -3
- package/README.md +229 -13
- package/package.json +37 -8
- package/skills/INVENTORY.md +88 -57
- package/skills/README.md +72 -6
- 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/checklists/cyfrin-defi-core/SKILL.md +3 -0
- package/skills/manifests/cyfrin.json +16 -0
- package/skills/manifests/defifofum.json +25 -0
- package/skills/manifests/kadenzipfel.json +48 -0
- package/skills/manifests/scvd.json +9 -0
- package/skills/manifests/smartbugs.json +9 -0
- package/skills/manifests/solodit.json +9 -0
- package/skills/manifests/sunweb3sec.json +9 -0
- package/skills/manifests/trailofbits.json +9 -0
- package/skills/methodology/audit-workflow/SKILL.md +3 -0
- package/skills/protocol-patterns/amm-dex/SKILL.md +3 -0
- package/skills/references/exploit-reference/SKILL.md +3 -0
- package/skills/vulnerability-patterns/access-control/SKILL.md +27 -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 +8 -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 +8 -1
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +14 -1
- 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 +13 -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 +22 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +11 -1
- package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +22 -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 +11 -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 +13 -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 +27 -10
- package/src/agents/pythia-prompt.ts +7 -8
- package/src/agents/scribe-prompt.ts +10 -5
- package/src/agents/sentinel-prompt.ts +36 -7
- package/src/cli/cli-output.ts +16 -0
- package/src/cli/cli-program.ts +29 -22
- package/src/cli/commands/check-skills.ts +135 -0
- package/src/cli/commands/doctor.ts +303 -23
- package/src/cli/commands/init.ts +8 -6
- package/src/cli/commands/install.ts +10 -8
- package/src/cli/commands/lint-skills.ts +118 -0
- package/src/cli/index.ts +5 -5
- package/src/cli/tui-prompts.ts +4 -2
- 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 +225 -29
- package/src/create-managers.ts +10 -8
- package/src/create-tools.ts +14 -8
- 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 +79 -19
- package/src/features/index.ts +5 -5
- package/src/features/persistent-state/audit-state-manager.ts +158 -52
- 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/agent-tracker.ts +53 -0
- package/src/hooks/compaction-hook.ts +46 -37
- package/src/hooks/config-handler.ts +31 -11
- package/src/hooks/context-budget.ts +42 -0
- package/src/hooks/event-hook.ts +48 -23
- package/src/hooks/hook-system.ts +4 -4
- package/src/hooks/index.ts +5 -5
- package/src/hooks/knowledge-sync-hook.ts +19 -21
- package/src/hooks/recon-context-builder.ts +66 -0
- package/src/hooks/safe-create-hook.ts +9 -11
- package/src/hooks/system-prompt-hook.ts +128 -0
- package/src/hooks/tool-tracking-hook.ts +162 -29
- package/src/hooks/types.ts +2 -1
- package/src/index.ts +23 -13
- package/src/knowledge/retry.ts +53 -0
- package/src/knowledge/scvd-client.ts +103 -83
- package/src/knowledge/scvd-errors.ts +89 -0
- package/src/knowledge/scvd-index.ts +110 -62
- package/src/knowledge/scvd-sync.ts +223 -47
- package/src/knowledge/source-manifest.ts +102 -0
- package/src/managers/index.ts +1 -1
- package/src/managers/types.ts +19 -14
- package/src/plugin-interface.ts +19 -8
- package/src/shared/binary-utils.ts +44 -34
- 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 +91 -17
- 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 +237 -0
- package/src/skills/skill-schema.ts +99 -0
- package/src/solodit-lifecycle.ts +202 -0
- package/src/state/audit-state.ts +10 -8
- package/src/state/finding-store.ts +68 -55
- package/src/state/types.ts +96 -44
- package/src/tools/argus-skill-load-tool.ts +78 -0
- 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 +206 -167
- package/src/tools/pattern-loader.ts +77 -0
- package/src/tools/pattern-schema.ts +51 -0
- package/src/tools/proxy-detection-tool.ts +224 -0
- package/src/tools/report-generator-tool.ts +333 -142
- package/src/tools/slither-tool.ts +300 -210
- package/src/tools/solodit-search-tool.ts +255 -80
- package/src/tools/sync-knowledge-tool.ts +7 -11
- package/src/utils/audit-artifact-detector.ts +118 -0
- package/src/utils/dependency-scanner.ts +93 -0
- package/src/utils/project-detector.ts +175 -86
- package/src/utils/solidity-parser.ts +112 -67
- package/src/utils/solodit-health.ts +29 -0
- package/src/hooks/event-hook-v2.ts +0 -99
- package/src/state/plugin-state.ts +0 -14
|
@@ -1,41 +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
|
|
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
|
|
56
|
-
severity: typeof obj
|
|
57
|
-
description: typeof obj
|
|
58
|
-
protocol: typeof obj
|
|
59
|
-
url: typeof obj
|
|
60
|
-
remediation: typeof obj
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
262
|
+
if (hasMcpError(response)) {
|
|
263
|
+
hadMcpError = true
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
99
266
|
|
|
100
|
-
|
|
101
|
-
|
|
267
|
+
const findings = filterFindingsBySeverity(
|
|
268
|
+
parseFindingsFromAnyResponse(response),
|
|
269
|
+
args.severity,
|
|
270
|
+
)
|
|
102
271
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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 =
|
|
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 {
|
|
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
|
|
@@ -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
|
+
}
|