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
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { withRetry } from "./knowledge/retry"
|
|
2
|
+
import { createLogger } from "./shared/logger"
|
|
3
|
+
import { checkSoloditHealth } from "./utils/solodit-health"
|
|
4
|
+
|
|
5
|
+
interface SoloditChildProcess {
|
|
6
|
+
kill(signal?: number): void
|
|
7
|
+
unref(): void
|
|
8
|
+
readonly exited: Promise<number | null>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let soloditChild: SoloditChildProcess | null = null
|
|
12
|
+
let monitorTimer: ReturnType<typeof setInterval> | null = null
|
|
13
|
+
let isRestarting = false
|
|
14
|
+
|
|
15
|
+
/** Whether the Solodit MCP server is currently available for tool calls. */
|
|
16
|
+
export let soloditAvailable = false
|
|
17
|
+
|
|
18
|
+
const DEFAULT_RESTART_SETTLE_MS = 2_000
|
|
19
|
+
const DEFAULT_RETRY_BASE_DELAY_MS = 1_000
|
|
20
|
+
const HEALTH_CHECK_INTERVAL_MS = 60_000
|
|
21
|
+
|
|
22
|
+
let restartSettleMs = DEFAULT_RESTART_SETTLE_MS
|
|
23
|
+
let retryBaseDelayMs = DEFAULT_RETRY_BASE_DELAY_MS
|
|
24
|
+
|
|
25
|
+
const defaultSpawnFn = (port: number): SoloditChildProcess =>
|
|
26
|
+
Bun.spawn(["npx", "-y", "@lyuboslavlyubenov/solodit-mcp"], {
|
|
27
|
+
stdin: "ignore",
|
|
28
|
+
stdout: "ignore",
|
|
29
|
+
stderr: "ignore",
|
|
30
|
+
env: { ...process.env, PORT: String(port) },
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
let spawnFn: (port: number) => SoloditChildProcess = defaultSpawnFn
|
|
34
|
+
|
|
35
|
+
/** Override internal timing and spawn for testing. */
|
|
36
|
+
export function _setTestConfig(config: {
|
|
37
|
+
restartSettleMs?: number
|
|
38
|
+
retryBaseDelayMs?: number
|
|
39
|
+
spawnFn?: (port: number) => SoloditChildProcess
|
|
40
|
+
}): void {
|
|
41
|
+
if (config.restartSettleMs !== undefined) restartSettleMs = config.restartSettleMs
|
|
42
|
+
if (config.retryBaseDelayMs !== undefined) retryBaseDelayMs = config.retryBaseDelayMs
|
|
43
|
+
if (config.spawnFn !== undefined) spawnFn = config.spawnFn
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function spawnSoloditChild(port: number): SoloditChildProcess {
|
|
47
|
+
const child = spawnFn(port)
|
|
48
|
+
child.unref()
|
|
49
|
+
return child
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function trackChildExit(child: SoloditChildProcess): void {
|
|
53
|
+
const logger = createLogger()
|
|
54
|
+
child.exited.then((code) => {
|
|
55
|
+
if (code !== 0 && code !== null) {
|
|
56
|
+
logger.warn(`Solodit MCP exited with code ${code}`)
|
|
57
|
+
}
|
|
58
|
+
if (soloditChild === child) {
|
|
59
|
+
soloditChild = null
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function restartSoloditMcp(port: number): Promise<boolean> {
|
|
65
|
+
const logger = createLogger()
|
|
66
|
+
|
|
67
|
+
if (soloditChild) {
|
|
68
|
+
try {
|
|
69
|
+
soloditChild.kill()
|
|
70
|
+
} catch {
|
|
71
|
+
logger.debug("Solodit MCP process already dead")
|
|
72
|
+
}
|
|
73
|
+
soloditChild = null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
soloditChild = spawnSoloditChild(port)
|
|
78
|
+
trackChildExit(soloditChild)
|
|
79
|
+
} catch (err) {
|
|
80
|
+
logger.warn("Failed to spawn Solodit MCP:", err)
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await Bun.sleep(restartSettleMs)
|
|
85
|
+
|
|
86
|
+
const result = await withRetry(
|
|
87
|
+
async () => {
|
|
88
|
+
const health = await checkSoloditHealth(port, true)
|
|
89
|
+
if (!health.reachable) throw new Error("Solodit not reachable after restart")
|
|
90
|
+
return health
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
maxAttempts: 3,
|
|
94
|
+
baseDelayMs: retryBaseDelayMs,
|
|
95
|
+
shouldRetry: () => true,
|
|
96
|
+
onRetry: (attempt) => logger.debug(`Solodit restart health retry ${attempt}`),
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if (result.success) {
|
|
101
|
+
soloditAvailable = true
|
|
102
|
+
logger.info("Solodit MCP restarted successfully")
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
logger.warn("Solodit MCP restart failed — will retry next cycle")
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function _runMonitoringCycle(port: number): Promise<void> {
|
|
111
|
+
if (isRestarting) return
|
|
112
|
+
const logger = createLogger()
|
|
113
|
+
try {
|
|
114
|
+
const health = await checkSoloditHealth(port, true)
|
|
115
|
+
if (health.reachable) {
|
|
116
|
+
if (!soloditAvailable) {
|
|
117
|
+
soloditAvailable = true
|
|
118
|
+
logger.info("Solodit MCP recovered — now available")
|
|
119
|
+
}
|
|
120
|
+
} else if (soloditAvailable) {
|
|
121
|
+
soloditAvailable = false
|
|
122
|
+
logger.warn("Solodit MCP health check failed, attempting restart...")
|
|
123
|
+
isRestarting = true
|
|
124
|
+
try {
|
|
125
|
+
await restartSoloditMcp(port)
|
|
126
|
+
} finally {
|
|
127
|
+
isRestarting = false
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
logger.debug("Monitoring cycle encountered an error")
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function startMonitoring(port: number): void {
|
|
136
|
+
if (monitorTimer) return
|
|
137
|
+
monitorTimer = setInterval(() => {
|
|
138
|
+
_runMonitoringCycle(port)
|
|
139
|
+
}, HEALTH_CHECK_INTERVAL_MS)
|
|
140
|
+
if (monitorTimer && typeof (monitorTimer as NodeJS.Timeout).unref === "function") {
|
|
141
|
+
;(monitorTimer as NodeJS.Timeout).unref()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Stop periodic health monitoring. */
|
|
146
|
+
export function stopSoloditMonitoring(): void {
|
|
147
|
+
if (monitorTimer) {
|
|
148
|
+
clearInterval(monitorTimer)
|
|
149
|
+
monitorTimer = null
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Reset all Solodit state — for testing only. */
|
|
154
|
+
export function _resetSoloditState(): void {
|
|
155
|
+
stopSoloditMonitoring()
|
|
156
|
+
soloditAvailable = false
|
|
157
|
+
isRestarting = false
|
|
158
|
+
restartSettleMs = DEFAULT_RESTART_SETTLE_MS
|
|
159
|
+
retryBaseDelayMs = DEFAULT_RETRY_BASE_DELAY_MS
|
|
160
|
+
spawnFn = defaultSpawnFn
|
|
161
|
+
if (soloditChild) {
|
|
162
|
+
try {
|
|
163
|
+
soloditChild.kill()
|
|
164
|
+
} catch {
|
|
165
|
+
createLogger().debug("Failed to kill Solodit MCP on reset")
|
|
166
|
+
}
|
|
167
|
+
soloditChild = null
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function startSoloditMcp(port: number): Promise<void> {
|
|
172
|
+
const logger = createLogger()
|
|
173
|
+
|
|
174
|
+
const health = await checkSoloditHealth(port, true)
|
|
175
|
+
if (health.reachable) {
|
|
176
|
+
logger.debug(`Solodit MCP already running on port ${port} — skipping spawn`)
|
|
177
|
+
soloditAvailable = true
|
|
178
|
+
startMonitoring(port)
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
soloditChild = spawnSoloditChild(port)
|
|
183
|
+
trackChildExit(soloditChild)
|
|
184
|
+
|
|
185
|
+
const deadline = AbortSignal.timeout(5000)
|
|
186
|
+
const delays = [1000, 2000]
|
|
187
|
+
for (const delay of delays) {
|
|
188
|
+
if (deadline.aborted) break
|
|
189
|
+
await Bun.sleep(delay)
|
|
190
|
+
if (deadline.aborted) break
|
|
191
|
+
const healthResult = await checkSoloditHealth(port, true)
|
|
192
|
+
if (healthResult.reachable) {
|
|
193
|
+
soloditAvailable = true
|
|
194
|
+
logger.debug(`Solodit MCP healthy on port ${port}`)
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!soloditAvailable) {
|
|
199
|
+
logger.warn(`Solodit MCP not reachable after startup — monitoring will retry`)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
startMonitoring(port)
|
|
203
|
+
}
|
package/src/state/audit-state.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { randomUUID } from "crypto"
|
|
2
|
-
import type
|
|
3
|
-
import {
|
|
1
|
+
import { randomUUID } from "node:crypto"
|
|
2
|
+
import { createFindingStore, type FindingStore } from "./finding-store"
|
|
3
|
+
import type { AuditState } from "./types"
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Factory function to create a new audit state instance (NOT singleton)
|
|
7
7
|
* Each call creates a fresh state with a unique session ID
|
|
8
8
|
*/
|
|
9
9
|
export function createAuditState(projectDir: string): {
|
|
10
|
-
state: AuditState
|
|
11
|
-
store: FindingStore
|
|
10
|
+
state: AuditState
|
|
11
|
+
store: FindingStore
|
|
12
12
|
} {
|
|
13
13
|
const state: AuditState = {
|
|
14
14
|
sessionId: randomUUID(),
|
|
@@ -21,9 +21,9 @@ export function createAuditState(projectDir: string): {
|
|
|
21
21
|
startTime: Date.now(),
|
|
22
22
|
soloditResults: [],
|
|
23
23
|
fuzzCounterexamples: [],
|
|
24
|
-
}
|
|
24
|
+
}
|
|
25
25
|
|
|
26
|
-
const store = createFindingStore(state)
|
|
26
|
+
const store = createFindingStore(state)
|
|
27
27
|
|
|
28
|
-
return { state, store }
|
|
28
|
+
return { state, store }
|
|
29
29
|
}
|
|
@@ -1,84 +1,98 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import type { AuditState, Finding, FindingSeverity } from "./types"
|
|
3
3
|
|
|
4
4
|
export interface FindingStore {
|
|
5
|
-
addFinding(finding: Omit<Finding, "id">): Finding
|
|
6
|
-
getFindings(filter?: {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}): Finding[];
|
|
10
|
-
hasFinding(check: string, file: string, lines: [number, number]): boolean;
|
|
11
|
-
serialize(): string;
|
|
5
|
+
addFinding(finding: Omit<Finding, "id">): Finding
|
|
6
|
+
getFindings(filter?: { severity?: FindingSeverity; source?: Finding["source"] }): Finding[]
|
|
7
|
+
hasFinding(check: string, file: string, lines: [number, number]): boolean
|
|
8
|
+
serialize(): string
|
|
12
9
|
}
|
|
13
10
|
|
|
14
11
|
/**
|
|
15
12
|
* Creates a finding store with deduplication by check+file+lines
|
|
16
13
|
* Deduplication key: `${check}:${file}:${lines[0]}-${lines[1]}`
|
|
17
14
|
*/
|
|
15
|
+
function isValidHydrationFinding(f: unknown): f is Finding {
|
|
16
|
+
if (typeof f !== "object" || f === null) return false
|
|
17
|
+
const obj = f as Record<string, unknown>
|
|
18
|
+
return (
|
|
19
|
+
typeof obj.check === "string" &&
|
|
20
|
+
obj.check.length > 0 &&
|
|
21
|
+
typeof obj.file === "string" &&
|
|
22
|
+
obj.file.length > 0 &&
|
|
23
|
+
Array.isArray(obj.lines) &&
|
|
24
|
+
obj.lines.length === 2 &&
|
|
25
|
+
typeof obj.lines[0] === "number" &&
|
|
26
|
+
typeof obj.lines[1] === "number"
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
export function createFindingStore(state: AuditState): FindingStore {
|
|
19
|
-
const findingMap = new Map<string, Finding>()
|
|
20
|
-
|
|
21
|
-
function generateId(
|
|
22
|
-
check
|
|
23
|
-
file: string,
|
|
24
|
-
lines: [number, number]
|
|
25
|
-
): string {
|
|
26
|
-
const key = `${check}:${file}:${lines[0]}-${lines[1]}`;
|
|
31
|
+
const findingMap = new Map<string, Finding>()
|
|
32
|
+
|
|
33
|
+
function generateId(check: string, file: string, lines: [number, number]): string {
|
|
34
|
+
const key = `${check}:${file}:${lines[0]}-${lines[1]}`
|
|
27
35
|
// Use deterministic hash for stable IDs
|
|
28
|
-
return createHash("sha256").update(key).digest("hex").substring(0, 16)
|
|
36
|
+
return createHash("sha256").update(key).digest("hex").substring(0, 16)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Hydrate findingMap from persisted state.findings
|
|
40
|
+
for (const f of state.findings) {
|
|
41
|
+
if (!isValidHydrationFinding(f)) continue
|
|
42
|
+
const id = generateId(f.check, f.file, f.lines)
|
|
43
|
+
if (!findingMap.has(id)) {
|
|
44
|
+
findingMap.set(id, { ...f, id })
|
|
45
|
+
}
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
function addFinding(finding: Omit<Finding, "id">): Finding {
|
|
32
|
-
const id = generateId(finding.check, finding.file, finding.lines)
|
|
49
|
+
const id = generateId(finding.check, finding.file, finding.lines)
|
|
33
50
|
|
|
34
51
|
// Check if finding already exists (deduplication)
|
|
35
|
-
|
|
36
|
-
|
|
52
|
+
const existing = findingMap.get(id)
|
|
53
|
+
if (existing) {
|
|
54
|
+
return existing
|
|
37
55
|
}
|
|
38
56
|
|
|
39
57
|
const newFinding: Finding = {
|
|
40
58
|
...finding,
|
|
41
59
|
id,
|
|
42
|
-
}
|
|
60
|
+
}
|
|
43
61
|
|
|
44
|
-
findingMap.set(id, newFinding)
|
|
45
|
-
state.findings.push(newFinding)
|
|
62
|
+
findingMap.set(id, newFinding)
|
|
63
|
+
state.findings.push(newFinding)
|
|
46
64
|
|
|
47
|
-
return newFinding
|
|
65
|
+
return newFinding
|
|
48
66
|
}
|
|
49
67
|
|
|
50
68
|
function getFindings(filter?: {
|
|
51
|
-
severity?: FindingSeverity
|
|
52
|
-
source?: Finding["source"]
|
|
69
|
+
severity?: FindingSeverity
|
|
70
|
+
source?: Finding["source"]
|
|
53
71
|
}): Finding[] {
|
|
54
72
|
if (!filter) {
|
|
55
|
-
return Array.from(findingMap.values())
|
|
73
|
+
return Array.from(findingMap.values())
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
return Array.from(findingMap.values()).filter((finding) => {
|
|
59
77
|
if (filter.severity && finding.severity !== filter.severity) {
|
|
60
|
-
return false
|
|
78
|
+
return false
|
|
61
79
|
}
|
|
62
80
|
if (filter.source && finding.source !== filter.source) {
|
|
63
|
-
return false
|
|
81
|
+
return false
|
|
64
82
|
}
|
|
65
|
-
return true
|
|
66
|
-
})
|
|
83
|
+
return true
|
|
84
|
+
})
|
|
67
85
|
}
|
|
68
86
|
|
|
69
|
-
function hasFinding(
|
|
70
|
-
check
|
|
71
|
-
|
|
72
|
-
lines: [number, number]
|
|
73
|
-
): boolean {
|
|
74
|
-
const id = generateId(check, file, lines);
|
|
75
|
-
return findingMap.has(id);
|
|
87
|
+
function hasFinding(check: string, file: string, lines: [number, number]): boolean {
|
|
88
|
+
const id = generateId(check, file, lines)
|
|
89
|
+
return findingMap.has(id)
|
|
76
90
|
}
|
|
77
91
|
|
|
78
92
|
function serialize(): string {
|
|
79
|
-
const findings = Array.from(findingMap.values())
|
|
80
|
-
const contractCount = state.contractsReviewed.length
|
|
81
|
-
const findingCount = findings.length
|
|
93
|
+
const findings = Array.from(findingMap.values())
|
|
94
|
+
const contractCount = state.contractsReviewed.length
|
|
95
|
+
const findingCount = findings.length
|
|
82
96
|
|
|
83
97
|
// Count by severity
|
|
84
98
|
const severityCounts: Record<FindingSeverity, number> = {
|
|
@@ -87,34 +101,33 @@ export function createFindingStore(state: AuditState): FindingStore {
|
|
|
87
101
|
Medium: 0,
|
|
88
102
|
Low: 0,
|
|
89
103
|
Informational: 0,
|
|
90
|
-
}
|
|
104
|
+
}
|
|
91
105
|
|
|
92
106
|
findings.forEach((finding) => {
|
|
93
|
-
severityCounts[finding.severity]
|
|
94
|
-
})
|
|
107
|
+
severityCounts[finding.severity]++
|
|
108
|
+
})
|
|
95
109
|
|
|
96
110
|
// Build severity string
|
|
97
|
-
const severityParts: string[] = []
|
|
111
|
+
const severityParts: string[] = []
|
|
98
112
|
if (severityCounts.Critical > 0) {
|
|
99
|
-
severityParts.push(`${severityCounts.Critical} Critical`)
|
|
113
|
+
severityParts.push(`${severityCounts.Critical} Critical`)
|
|
100
114
|
}
|
|
101
115
|
if (severityCounts.High > 0) {
|
|
102
|
-
severityParts.push(`${severityCounts.High} High`)
|
|
116
|
+
severityParts.push(`${severityCounts.High} High`)
|
|
103
117
|
}
|
|
104
118
|
if (severityCounts.Medium > 0) {
|
|
105
|
-
severityParts.push(`${severityCounts.Medium} Medium`)
|
|
119
|
+
severityParts.push(`${severityCounts.Medium} Medium`)
|
|
106
120
|
}
|
|
107
121
|
if (severityCounts.Low > 0) {
|
|
108
|
-
severityParts.push(`${severityCounts.Low} Low`)
|
|
122
|
+
severityParts.push(`${severityCounts.Low} Low`)
|
|
109
123
|
}
|
|
110
124
|
if (severityCounts.Informational > 0) {
|
|
111
|
-
severityParts.push(`${severityCounts.Informational} Informational`)
|
|
125
|
+
severityParts.push(`${severityCounts.Informational} Informational`)
|
|
112
126
|
}
|
|
113
127
|
|
|
114
|
-
const severityStr =
|
|
115
|
-
severityParts.length > 0 ? ` (${severityParts.join(", ")})` : "";
|
|
128
|
+
const severityStr = severityParts.length > 0 ? ` (${severityParts.join(", ")})` : ""
|
|
116
129
|
|
|
117
|
-
return `Contracts: ${contractCount}, Findings: ${findingCount}${severityStr}, Phase: ${state.currentPhase}
|
|
130
|
+
return `Contracts: ${contractCount}, Findings: ${findingCount}${severityStr}, Phase: ${state.currentPhase}`
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
return {
|
|
@@ -122,5 +135,5 @@ export function createFindingStore(state: AuditState): FindingStore {
|
|
|
122
135
|
getFindings,
|
|
123
136
|
hasFinding,
|
|
124
137
|
serialize,
|
|
125
|
-
}
|
|
138
|
+
}
|
|
126
139
|
}
|
package/src/state/types.ts
CHANGED
|
@@ -1,92 +1,113 @@
|
|
|
1
|
-
export type FindingSeverity = "Critical" | "High" | "Medium" | "Low" | "Informational"
|
|
2
|
-
export type AuditPhase =
|
|
1
|
+
export type FindingSeverity = "Critical" | "High" | "Medium" | "Low" | "Informational"
|
|
2
|
+
export type AuditPhase =
|
|
3
|
+
| "reconnaissance"
|
|
4
|
+
| "scanning"
|
|
5
|
+
| "manual-review"
|
|
6
|
+
| "attack-surface"
|
|
7
|
+
| "research"
|
|
8
|
+
| "testing"
|
|
9
|
+
| "reporting"
|
|
10
|
+
| "complete"
|
|
3
11
|
|
|
4
12
|
export interface Finding {
|
|
5
|
-
id: string
|
|
6
|
-
check: string
|
|
7
|
-
severity: FindingSeverity
|
|
8
|
-
confidence: "High" | "Medium" | "Low"
|
|
9
|
-
description: string
|
|
10
|
-
file: string
|
|
11
|
-
lines: [number, number]
|
|
12
|
-
source: "slither" | "manual" | "pattern" | "scvd" | "solodit" | "fuzz"
|
|
13
|
-
remediation?: string
|
|
14
|
-
exploitReference?: string
|
|
13
|
+
id: string // unique hash: check+file+lines
|
|
14
|
+
check: string // detector name e.g. "reentrancy-eth"
|
|
15
|
+
severity: FindingSeverity
|
|
16
|
+
confidence: "High" | "Medium" | "Low"
|
|
17
|
+
description: string
|
|
18
|
+
file: string // relative file path
|
|
19
|
+
lines: [number, number] // [start, end]
|
|
20
|
+
source: "slither" | "manual" | "pattern" | "scvd" | "solodit" | "fuzz"
|
|
21
|
+
remediation?: string
|
|
22
|
+
exploitReference?: string
|
|
15
23
|
provenance?: {
|
|
16
|
-
timestamp: number
|
|
17
|
-
toolVersion?: string
|
|
18
|
-
phase?: AuditPhase
|
|
19
|
-
}
|
|
24
|
+
timestamp: number
|
|
25
|
+
toolVersion?: string
|
|
26
|
+
phase?: AuditPhase
|
|
27
|
+
}
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
export interface SoloditResult {
|
|
23
|
-
query: string
|
|
24
|
-
timestamp: number
|
|
25
|
-
resultCount: number
|
|
31
|
+
query: string
|
|
32
|
+
timestamp: number
|
|
33
|
+
resultCount: number
|
|
26
34
|
topResults: Array<{
|
|
27
|
-
title: string
|
|
28
|
-
severity: string
|
|
29
|
-
url: string
|
|
30
|
-
protocol: string
|
|
31
|
-
}
|
|
35
|
+
title: string
|
|
36
|
+
severity: string
|
|
37
|
+
url: string
|
|
38
|
+
protocol: string
|
|
39
|
+
}>
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
export interface FuzzCounterexample {
|
|
35
|
-
testName: string
|
|
36
|
-
inputs: string[]
|
|
37
|
-
revertReason?: string
|
|
38
|
-
runs: number
|
|
39
|
-
seed?: number
|
|
40
|
-
timestamp: number
|
|
43
|
+
testName: string
|
|
44
|
+
inputs: string[]
|
|
45
|
+
revertReason?: string
|
|
46
|
+
runs: number
|
|
47
|
+
seed?: number
|
|
48
|
+
timestamp: number
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
export interface ContractProfile {
|
|
44
|
-
name: string
|
|
45
|
-
filePath: string
|
|
52
|
+
name: string
|
|
53
|
+
filePath: string
|
|
46
54
|
functions: Array<{
|
|
47
|
-
name: string
|
|
48
|
-
visibility: string
|
|
49
|
-
mutability: string
|
|
50
|
-
modifiers: string[]
|
|
51
|
-
}
|
|
55
|
+
name: string
|
|
56
|
+
visibility: string
|
|
57
|
+
mutability: string
|
|
58
|
+
modifiers: string[]
|
|
59
|
+
}>
|
|
52
60
|
stateVars: Array<{
|
|
53
|
-
name: string
|
|
54
|
-
type: string
|
|
55
|
-
visibility: string
|
|
56
|
-
}
|
|
57
|
-
inheritance: string[]
|
|
58
|
-
accessControlPattern?: "ownable" | "access-control" | "custom" | "none"
|
|
59
|
-
externalCalls: string[]
|
|
60
|
-
riskIndicators: string[]
|
|
61
|
-
error?: string
|
|
61
|
+
name: string
|
|
62
|
+
type: string
|
|
63
|
+
visibility: string
|
|
64
|
+
}>
|
|
65
|
+
inheritance: string[]
|
|
66
|
+
accessControlPattern?: "ownable" | "access-control" | "custom" | "none"
|
|
67
|
+
externalCalls: string[]
|
|
68
|
+
riskIndicators: string[]
|
|
69
|
+
error?: string
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
export interface ToolExecution {
|
|
65
|
-
tool: string
|
|
66
|
-
startTime: number
|
|
67
|
-
endTime?: number
|
|
68
|
-
success: boolean
|
|
69
|
-
findingsCount: number
|
|
73
|
+
tool: string
|
|
74
|
+
startTime: number
|
|
75
|
+
endTime?: number
|
|
76
|
+
success: boolean
|
|
77
|
+
findingsCount: number
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
export interface AuditState {
|
|
73
|
-
sessionId: string
|
|
74
|
-
projectDir: string
|
|
75
|
-
contractsReviewed: string[]
|
|
76
|
-
findings: Finding[]
|
|
77
|
-
toolsExecuted: ToolExecution[]
|
|
78
|
-
currentPhase: AuditPhase
|
|
79
|
-
scope: string[]
|
|
80
|
-
startTime: number
|
|
81
|
-
soloditResults?: SoloditResult[]
|
|
82
|
-
fuzzCounterexamples?: FuzzCounterexample[]
|
|
83
|
-
patternVersion?: string
|
|
84
|
-
skillsLoaded?: string[]
|
|
85
|
-
unavailableTools?: string[]
|
|
81
|
+
sessionId: string
|
|
82
|
+
projectDir: string
|
|
83
|
+
contractsReviewed: string[]
|
|
84
|
+
findings: Finding[]
|
|
85
|
+
toolsExecuted: ToolExecution[]
|
|
86
|
+
currentPhase: AuditPhase
|
|
87
|
+
scope: string[]
|
|
88
|
+
startTime: number
|
|
89
|
+
soloditResults?: SoloditResult[]
|
|
90
|
+
fuzzCounterexamples?: FuzzCounterexample[]
|
|
91
|
+
patternVersion?: string
|
|
92
|
+
skillsLoaded?: string[]
|
|
93
|
+
unavailableTools?: string[]
|
|
94
|
+
reportGenerated?: boolean
|
|
95
|
+
knowledgeSynced?: { success: boolean; timestamp: number }
|
|
96
|
+
coverageReport?: {
|
|
97
|
+
files: Array<{
|
|
98
|
+
path: string
|
|
99
|
+
linesPct: number
|
|
100
|
+
statementsPct: number
|
|
101
|
+
branchesPct: number
|
|
102
|
+
functionsPct: number
|
|
103
|
+
}>
|
|
104
|
+
}
|
|
105
|
+
gasHotspots?: Array<{ contract: string; function: string; avgGas: number }>
|
|
106
|
+
proxyContracts?: Array<{ file: string; proxyType: string; indicators: string[] }>
|
|
86
107
|
}
|
|
87
108
|
|
|
88
109
|
export interface PersistentAuditState extends AuditState {
|
|
89
|
-
savedAt: number
|
|
90
|
-
version: string
|
|
91
|
-
filePath: string
|
|
110
|
+
savedAt: number
|
|
111
|
+
version: string
|
|
112
|
+
filePath: string
|
|
92
113
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ToolContext, tool } from "@opencode-ai/plugin"
|
|
2
2
|
import { loadArgusConfig } from "../config/loader"
|
|
3
3
|
import type { ArgusConfig } from "../config/types"
|
|
4
|
+
import { resolveProjectDir } from "../shared/project-utils"
|
|
4
5
|
import { normalizeSkillName, resolveArgusSkills } from "../skills/argus-skill-resolver"
|
|
5
6
|
|
|
6
7
|
type ArgusSkillLoadArgs = {
|
|
@@ -15,9 +16,9 @@ type ArgusSkillLoadDependencies = {
|
|
|
15
16
|
export async function executeArgusSkillLoad(
|
|
16
17
|
args: ArgusSkillLoadArgs,
|
|
17
18
|
context: ToolContext,
|
|
18
|
-
deps: ArgusSkillLoadDependencies = {}
|
|
19
|
+
deps: ArgusSkillLoadDependencies = {},
|
|
19
20
|
): Promise<string> {
|
|
20
|
-
const projectDir = context
|
|
21
|
+
const projectDir = resolveProjectDir(context)
|
|
21
22
|
const loadConfig = deps.loadConfig ?? loadArgusConfig
|
|
22
23
|
const resolveSkills = deps.resolveSkills ?? resolveArgusSkills
|
|
23
24
|
|
|
@@ -35,7 +36,7 @@ export async function executeArgusSkillLoad(
|
|
|
35
36
|
if (!skill) {
|
|
36
37
|
const available = Array.from(skills.keys()).sort().join(", ")
|
|
37
38
|
throw new Error(
|
|
38
|
-
`Argus skill "${args.name}" not found (normalized: "${normalizedName}"). Available Argus skills: ${available || "none"}
|
|
39
|
+
`Argus skill "${args.name}" not found (normalized: "${normalizedName}"). Available Argus skills: ${available || "none"}`,
|
|
39
40
|
)
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -44,7 +45,8 @@ export async function executeArgusSkillLoad(
|
|
|
44
45
|
if (skill.source_url) provenanceParts.push(skill.source_url)
|
|
45
46
|
if (skill.imported_at) provenanceParts.push(`Imported: ${skill.imported_at}`)
|
|
46
47
|
|
|
47
|
-
const provenanceLine =
|
|
48
|
+
const provenanceLine =
|
|
49
|
+
provenanceParts.length > 0 ? `[Provenance: ${provenanceParts.join(" | ")}]` : ""
|
|
48
50
|
|
|
49
51
|
return [
|
|
50
52
|
`## Argus Skill: ${skill.name} [Source: ${skill.source}]`,
|
|
@@ -61,11 +63,14 @@ export async function executeArgusSkillLoad(
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
export const argusSkillLoadTool = tool({
|
|
64
|
-
description:
|
|
66
|
+
description:
|
|
67
|
+
"Load Argus security skill content with OMO-compatible discovery and native fallback.",
|
|
65
68
|
args: {
|
|
66
69
|
name: tool.schema
|
|
67
70
|
.string()
|
|
68
|
-
.describe(
|
|
71
|
+
.describe(
|
|
72
|
+
"Skill name (e.g., reentrancy, oracle-manipulation, or vulnerability-patterns/reentrancy).",
|
|
73
|
+
),
|
|
69
74
|
},
|
|
70
75
|
async execute(args, context) {
|
|
71
76
|
return executeArgusSkillLoad(args, context)
|