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,96 +1,78 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { extname, join, resolve } from "node:path"
|
|
4
|
-
import {
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs"
|
|
2
|
+
import os from "node:os"
|
|
3
|
+
import { dirname, extname, join, resolve } from "node:path"
|
|
4
|
+
import { type ToolContext, tool } from "@opencode-ai/plugin"
|
|
5
5
|
import {
|
|
6
6
|
loadIndex,
|
|
7
|
-
searchIndex,
|
|
8
7
|
type ScvdIndex,
|
|
9
8
|
type ScvdIndexEntry,
|
|
10
|
-
|
|
9
|
+
searchIndex,
|
|
10
|
+
} from "../knowledge/scvd-index"
|
|
11
|
+
import { createLogger } from "../shared/logger"
|
|
12
|
+
import { extractDetectionRulesFromSkills } from "./pattern-loader"
|
|
13
|
+
import type { PatternDefinition } from "./pattern-schema"
|
|
14
|
+
|
|
15
|
+
const logger = createLogger()
|
|
16
|
+
|
|
17
|
+
export type PatternSource = "skill"
|
|
11
18
|
|
|
12
19
|
export interface Match {
|
|
13
|
-
pattern: string
|
|
14
|
-
severity: "Critical" | "High" | "Medium" | "Low" | "Informational"
|
|
15
|
-
file: string
|
|
16
|
-
lines: [number, number]
|
|
17
|
-
description: string
|
|
18
|
-
exploitReference?: string
|
|
20
|
+
pattern: string
|
|
21
|
+
severity: "Critical" | "High" | "Medium" | "Low" | "Informational"
|
|
22
|
+
file: string
|
|
23
|
+
lines: [number, number]
|
|
24
|
+
description: string
|
|
25
|
+
exploitReference?: string
|
|
26
|
+
patternSource?: PatternSource
|
|
27
|
+
category?: string
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
export interface MatchSource {
|
|
22
|
-
source: string
|
|
23
|
-
matches: Match[]
|
|
31
|
+
source: string
|
|
32
|
+
matches: Match[]
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
export interface PatternCheckResult {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
success: boolean
|
|
37
|
+
error?: string
|
|
38
|
+
matches: Match[]
|
|
39
|
+
summary: {
|
|
40
|
+
total: number
|
|
41
|
+
bySeverity: Record<string, number>
|
|
42
|
+
byCategory: Record<string, number>
|
|
43
|
+
}
|
|
44
|
+
sources: MatchSource[]
|
|
45
|
+
patternsChecked: number
|
|
46
|
+
executionTime: number
|
|
47
|
+
target: string
|
|
48
|
+
patternVersion?: string
|
|
31
49
|
}
|
|
32
50
|
|
|
33
51
|
type PatternCheckArgs = {
|
|
34
|
-
target: string
|
|
35
|
-
patterns?: string[]
|
|
36
|
-
include_scvd?: boolean
|
|
37
|
-
}
|
|
52
|
+
target: string
|
|
53
|
+
patterns?: string[]
|
|
54
|
+
include_scvd?: boolean
|
|
55
|
+
}
|
|
38
56
|
|
|
39
57
|
type PatternCheckDependencies = {
|
|
40
|
-
loadIndexFn?: (filePath: string) => Promise<ScvdIndex | null
|
|
58
|
+
loadIndexFn?: (filePath: string) => Promise<ScvdIndex | null>
|
|
41
59
|
searchIndexFn?: (
|
|
42
60
|
index: ScvdIndex,
|
|
43
|
-
query: { swc?: string; severity?: string; keyword?: string; limit?: number }
|
|
44
|
-
) => ScvdIndexEntry[]
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
type
|
|
48
|
-
name: string
|
|
49
|
-
category: string
|
|
50
|
-
severity: Match["severity"]
|
|
51
|
-
regex: RegExp
|
|
52
|
-
description: string
|
|
53
|
-
exploitReference?: string
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
name: "reentrancy",
|
|
59
|
-
category: "reentrancy",
|
|
60
|
-
severity: "High",
|
|
61
|
-
regex: /\.call\{value:/,
|
|
62
|
-
description: "Potential reentrancy: ETH transfer via low-level call",
|
|
63
|
-
exploitReference: "DAO hack ($60M), 2016",
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
name: "tx-origin-auth",
|
|
67
|
-
category: "access-control",
|
|
68
|
-
severity: "High",
|
|
69
|
-
regex: /tx\.origin/,
|
|
70
|
-
description: "Use of tx.origin for authorization - vulnerable to phishing",
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
name: "selfdestruct",
|
|
74
|
-
category: "access-control",
|
|
75
|
-
severity: "High",
|
|
76
|
-
regex: /selfdestruct\(|suicide\(/,
|
|
77
|
-
description: "Contract uses selfdestruct - can destroy contract",
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
name: "delegatecall",
|
|
81
|
-
category: "delegatecall",
|
|
82
|
-
severity: "High",
|
|
83
|
-
regex: /\.delegatecall\(/,
|
|
84
|
-
description: "Use of delegatecall - can overwrite storage",
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: "missing-zero-check",
|
|
88
|
-
category: "access-control",
|
|
89
|
-
severity: "Medium",
|
|
90
|
-
regex: /address\(0\)/,
|
|
91
|
-
description: "Potential missing zero-address validation",
|
|
92
|
-
},
|
|
93
|
-
];
|
|
61
|
+
query: { swc?: string; severity?: string; keyword?: string; limit?: number },
|
|
62
|
+
) => ScvdIndexEntry[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type LoadedPattern = {
|
|
66
|
+
name: string
|
|
67
|
+
category: string
|
|
68
|
+
severity: Match["severity"]
|
|
69
|
+
regex: RegExp
|
|
70
|
+
description: string
|
|
71
|
+
exploitReference?: string
|
|
72
|
+
source?: PatternSource
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const PATTERN_PACK_VERSION = "1.0.0"
|
|
94
76
|
|
|
95
77
|
const CATEGORY_TO_SWC: Record<string, string[]> = {
|
|
96
78
|
reentrancy: ["SWC-107"],
|
|
@@ -99,66 +81,82 @@ const CATEGORY_TO_SWC: Record<string, string[]> = {
|
|
|
99
81
|
delegatecall: ["SWC-112"],
|
|
100
82
|
"signature-replay": ["SWC-121"],
|
|
101
83
|
"integer-overflow": ["SWC-101"],
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
84
|
+
governance: ["SWC-105", "SWC-106"],
|
|
85
|
+
"front-running": ["SWC-114"],
|
|
86
|
+
"logic-error": ["SWC-101", "SWC-116"],
|
|
87
|
+
"gas-optimization": ["SWC-128"],
|
|
88
|
+
dos: ["SWC-128"],
|
|
89
|
+
}
|
|
107
90
|
|
|
108
91
|
function normalizeSeverity(value: string): Match["severity"] {
|
|
109
|
-
if (value === "Critical") return "Critical"
|
|
110
|
-
if (value === "High") return "High"
|
|
111
|
-
if (value === "Medium") return "Medium"
|
|
112
|
-
if (value === "Low") return "Low"
|
|
113
|
-
return "Informational"
|
|
92
|
+
if (value === "Critical") return "Critical"
|
|
93
|
+
if (value === "High") return "High"
|
|
94
|
+
if (value === "Medium") return "Medium"
|
|
95
|
+
if (value === "Low") return "Low"
|
|
96
|
+
return "Informational"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizePatternDefinitions(
|
|
100
|
+
patterns: PatternDefinition[],
|
|
101
|
+
source: PatternSource,
|
|
102
|
+
): LoadedPattern[] {
|
|
103
|
+
return patterns.map((patternDef) => ({
|
|
104
|
+
name: patternDef.name,
|
|
105
|
+
category: patternDef.category,
|
|
106
|
+
severity: patternDef.severity,
|
|
107
|
+
regex: new RegExp(patternDef.regex),
|
|
108
|
+
description: patternDef.description,
|
|
109
|
+
...(patternDef.exploit_ref ? { exploitReference: patternDef.exploit_ref } : {}),
|
|
110
|
+
source,
|
|
111
|
+
}))
|
|
114
112
|
}
|
|
115
113
|
|
|
116
114
|
function uniqueScvdEntries(entries: ScvdIndexEntry[]): ScvdIndexEntry[] {
|
|
117
|
-
const deduped = new Map<string, ScvdIndexEntry>()
|
|
115
|
+
const deduped = new Map<string, ScvdIndexEntry>()
|
|
118
116
|
for (const entry of entries) {
|
|
119
|
-
deduped.set(entry.id, entry)
|
|
117
|
+
deduped.set(entry.id, entry)
|
|
120
118
|
}
|
|
121
|
-
return Array.from(deduped.values())
|
|
119
|
+
return Array.from(deduped.values())
|
|
122
120
|
}
|
|
123
121
|
|
|
124
122
|
async function collectScvdMatches(
|
|
125
123
|
matches: Match[],
|
|
126
|
-
dependencies: Required<PatternCheckDependencies
|
|
124
|
+
dependencies: Required<PatternCheckDependencies>,
|
|
127
125
|
): Promise<Match[]> {
|
|
128
|
-
const detectedCategories = new Set<string>()
|
|
126
|
+
const detectedCategories = new Set<string>()
|
|
129
127
|
for (const match of matches) {
|
|
130
|
-
const category =
|
|
128
|
+
const category = match.category
|
|
131
129
|
if (category) {
|
|
132
|
-
detectedCategories.add(category)
|
|
130
|
+
detectedCategories.add(category)
|
|
133
131
|
}
|
|
134
132
|
}
|
|
135
133
|
|
|
136
134
|
if (detectedCategories.size === 0) {
|
|
137
|
-
return []
|
|
135
|
+
return []
|
|
138
136
|
}
|
|
139
137
|
|
|
140
|
-
const swcCodes = new Set<string>()
|
|
138
|
+
const swcCodes = new Set<string>()
|
|
141
139
|
for (const category of detectedCategories) {
|
|
142
|
-
const mappedSwcs = CATEGORY_TO_SWC[category] ?? []
|
|
140
|
+
const mappedSwcs = CATEGORY_TO_SWC[category] ?? []
|
|
143
141
|
for (const swcCode of mappedSwcs) {
|
|
144
|
-
swcCodes.add(swcCode)
|
|
142
|
+
swcCodes.add(swcCode)
|
|
145
143
|
}
|
|
146
144
|
}
|
|
147
145
|
|
|
148
146
|
if (swcCodes.size === 0) {
|
|
149
|
-
return []
|
|
147
|
+
return []
|
|
150
148
|
}
|
|
151
149
|
|
|
152
|
-
const indexPath = join(os.homedir(), ".cache", "solidity-argus", "scvd-index.json")
|
|
153
|
-
const index = await dependencies.loadIndexFn(indexPath)
|
|
150
|
+
const indexPath = join(os.homedir(), ".cache", "solidity-argus", "scvd-index.json")
|
|
151
|
+
const index = await dependencies.loadIndexFn(indexPath)
|
|
154
152
|
|
|
155
153
|
if (!index) {
|
|
156
|
-
return []
|
|
154
|
+
return []
|
|
157
155
|
}
|
|
158
156
|
|
|
159
|
-
const entries: ScvdIndexEntry[] = []
|
|
157
|
+
const entries: ScvdIndexEntry[] = []
|
|
160
158
|
for (const swcCode of swcCodes) {
|
|
161
|
-
entries.push(...dependencies.searchIndexFn(index, { swc: swcCode }))
|
|
159
|
+
entries.push(...dependencies.searchIndexFn(index, { swc: swcCode }))
|
|
162
160
|
}
|
|
163
161
|
|
|
164
162
|
return uniqueScvdEntries(entries).map((entry) => ({
|
|
@@ -168,83 +166,86 @@ async function collectScvdMatches(
|
|
|
168
166
|
lines: [1, 1],
|
|
169
167
|
description: entry.title,
|
|
170
168
|
exploitReference: entry.repoUrl,
|
|
171
|
-
}))
|
|
169
|
+
}))
|
|
172
170
|
}
|
|
173
171
|
|
|
174
|
-
function collectSolidityFiles(target: string): string[] {
|
|
175
|
-
const absoluteTarget = resolve(target)
|
|
176
|
-
let stats: ReturnType<typeof statSync
|
|
172
|
+
function collectSolidityFiles(target: string, maxDepth = 8): string[] {
|
|
173
|
+
const absoluteTarget = resolve(target)
|
|
174
|
+
let stats: ReturnType<typeof statSync>
|
|
177
175
|
|
|
178
176
|
try {
|
|
179
|
-
stats = statSync(absoluteTarget)
|
|
177
|
+
stats = statSync(absoluteTarget)
|
|
180
178
|
} catch {
|
|
181
|
-
|
|
179
|
+
return []
|
|
182
180
|
}
|
|
183
181
|
|
|
184
182
|
if (stats.isFile()) {
|
|
185
|
-
return extname(absoluteTarget) === ".sol" ? [absoluteTarget] : []
|
|
183
|
+
return extname(absoluteTarget) === ".sol" ? [absoluteTarget] : []
|
|
186
184
|
}
|
|
187
185
|
|
|
188
186
|
if (!stats.isDirectory()) {
|
|
189
|
-
return []
|
|
187
|
+
return []
|
|
190
188
|
}
|
|
191
189
|
|
|
192
|
-
const discovered: string[] = []
|
|
193
|
-
const stack = [absoluteTarget]
|
|
190
|
+
const discovered: string[] = []
|
|
191
|
+
const stack: Array<{ path: string; depth: number }> = [{ path: absoluteTarget, depth: 0 }]
|
|
194
192
|
|
|
195
193
|
while (stack.length > 0) {
|
|
196
|
-
const current = stack.pop()
|
|
197
|
-
if (!current) {
|
|
198
|
-
continue
|
|
194
|
+
const current = stack.pop()
|
|
195
|
+
if (!current || current.depth > maxDepth) {
|
|
196
|
+
continue
|
|
199
197
|
}
|
|
200
198
|
|
|
201
|
-
const entries = readdirSync(current, { withFileTypes: true })
|
|
199
|
+
const entries = readdirSync(current.path, { withFileTypes: true })
|
|
202
200
|
for (const entry of entries) {
|
|
203
|
-
const fullPath = resolve(current, entry.name)
|
|
201
|
+
const fullPath = resolve(current.path, entry.name)
|
|
204
202
|
if (entry.isDirectory()) {
|
|
205
|
-
stack.push(fullPath)
|
|
206
|
-
continue
|
|
203
|
+
stack.push({ path: fullPath, depth: current.depth + 1 })
|
|
204
|
+
continue
|
|
207
205
|
}
|
|
208
206
|
|
|
209
207
|
if (entry.isFile() && extname(entry.name) === ".sol") {
|
|
210
|
-
discovered.push(fullPath)
|
|
208
|
+
discovered.push(fullPath)
|
|
211
209
|
}
|
|
212
210
|
}
|
|
213
211
|
}
|
|
214
212
|
|
|
215
|
-
return discovered
|
|
213
|
+
return discovered
|
|
216
214
|
}
|
|
217
215
|
|
|
218
216
|
function lineNumberAt(content: string, index: number): number {
|
|
219
217
|
if (index <= 0) {
|
|
220
|
-
return 1
|
|
218
|
+
return 1
|
|
221
219
|
}
|
|
222
220
|
|
|
223
|
-
let line = 1
|
|
221
|
+
let line = 1
|
|
224
222
|
for (let i = 0; i < index && i < content.length; i += 1) {
|
|
225
223
|
if (content[i] === "\n") {
|
|
226
|
-
line += 1
|
|
224
|
+
line += 1
|
|
227
225
|
}
|
|
228
226
|
}
|
|
229
|
-
return line
|
|
227
|
+
return line
|
|
230
228
|
}
|
|
231
229
|
|
|
232
230
|
function lineWindow(content: string, index: number): [number, number] {
|
|
233
|
-
const linesCount = content.split("\n").length
|
|
234
|
-
const line = lineNumberAt(content, index)
|
|
235
|
-
const start = Math.max(1, line - 5)
|
|
236
|
-
const end = Math.min(linesCount, line + 5)
|
|
237
|
-
return [start, end]
|
|
231
|
+
const linesCount = content.split("\n").length
|
|
232
|
+
const line = lineNumberAt(content, index)
|
|
233
|
+
const start = Math.max(1, line - 5)
|
|
234
|
+
const end = Math.min(linesCount, line + 5)
|
|
235
|
+
return [start, end]
|
|
238
236
|
}
|
|
239
237
|
|
|
240
|
-
function findMatches(file: string, patterns:
|
|
241
|
-
const content = readFileSync(file, "utf8")
|
|
242
|
-
const matches: Match[] = []
|
|
238
|
+
function findMatches(file: string, patterns: LoadedPattern[]): Match[] {
|
|
239
|
+
const content = readFileSync(file, "utf8")
|
|
240
|
+
const matches: Match[] = []
|
|
243
241
|
|
|
244
242
|
for (const pattern of patterns) {
|
|
245
|
-
const regex = new RegExp(
|
|
243
|
+
const regex = new RegExp(
|
|
244
|
+
pattern.regex.source,
|
|
245
|
+
pattern.regex.flags.includes("g") ? pattern.regex.flags : `${pattern.regex.flags}g`,
|
|
246
|
+
)
|
|
246
247
|
for (const found of content.matchAll(regex)) {
|
|
247
|
-
const index = found.index ?? 0
|
|
248
|
+
const index = found.index ?? 0
|
|
248
249
|
matches.push({
|
|
249
250
|
pattern: pattern.name,
|
|
250
251
|
severity: pattern.severity,
|
|
@@ -252,48 +253,70 @@ function findMatches(file: string, patterns: BuiltinPattern[]): Match[] {
|
|
|
252
253
|
lines: lineWindow(content, index),
|
|
253
254
|
description: pattern.description,
|
|
254
255
|
exploitReference: pattern.exploitReference,
|
|
255
|
-
|
|
256
|
+
patternSource: pattern.source ?? "skill",
|
|
257
|
+
category: pattern.category,
|
|
258
|
+
})
|
|
256
259
|
}
|
|
257
260
|
}
|
|
258
261
|
|
|
259
|
-
return matches
|
|
262
|
+
return matches
|
|
260
263
|
}
|
|
261
264
|
|
|
262
|
-
function selectPatterns(
|
|
265
|
+
function selectPatterns(
|
|
266
|
+
availablePatterns: LoadedPattern[],
|
|
267
|
+
categories?: string[],
|
|
268
|
+
): LoadedPattern[] {
|
|
263
269
|
if (!categories || categories.length === 0) {
|
|
264
|
-
return
|
|
270
|
+
return availablePatterns
|
|
265
271
|
}
|
|
266
272
|
|
|
267
|
-
const set = new Set(categories)
|
|
268
|
-
return
|
|
273
|
+
const set = new Set(categories)
|
|
274
|
+
return availablePatterns.filter((pattern) => set.has(pattern.category))
|
|
269
275
|
}
|
|
270
276
|
|
|
271
277
|
export async function executePatternCheck(
|
|
272
278
|
args: PatternCheckArgs,
|
|
273
279
|
context: ToolContext,
|
|
274
|
-
deps: PatternCheckDependencies = {}
|
|
280
|
+
deps: PatternCheckDependencies = {},
|
|
275
281
|
): Promise<PatternCheckResult> {
|
|
276
282
|
const dependencies: Required<PatternCheckDependencies> = {
|
|
277
283
|
loadIndexFn: loadIndex,
|
|
278
284
|
searchIndexFn: searchIndex,
|
|
279
285
|
...deps,
|
|
280
|
-
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const startedAt = Date.now()
|
|
289
|
+
context.metadata({ title: `Pattern check: ${args.target}` })
|
|
281
290
|
|
|
282
|
-
const
|
|
283
|
-
|
|
291
|
+
const skillsDir = join(dirname(dirname(__dirname)), "skills")
|
|
292
|
+
const skillDetectionRules = extractDetectionRulesFromSkills(skillsDir)
|
|
284
293
|
|
|
285
|
-
const
|
|
286
|
-
|
|
294
|
+
const allPatterns: LoadedPattern[] = [
|
|
295
|
+
...normalizePatternDefinitions(skillDetectionRules, "skill"),
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
const selectedPatterns = selectPatterns(allPatterns, args.patterns)
|
|
299
|
+
const solidityFiles = collectSolidityFiles(args.target)
|
|
287
300
|
if (solidityFiles.length === 0) {
|
|
288
|
-
|
|
301
|
+
return {
|
|
302
|
+
success: false,
|
|
303
|
+
error: `No Solidity files found for target: ${args.target}`,
|
|
304
|
+
matches: [],
|
|
305
|
+
summary: { total: 0, bySeverity: {}, byCategory: {} },
|
|
306
|
+
sources: [],
|
|
307
|
+
patternsChecked: selectedPatterns.length,
|
|
308
|
+
executionTime: Date.now() - startedAt,
|
|
309
|
+
target: args.target,
|
|
310
|
+
patternVersion: PATTERN_PACK_VERSION,
|
|
311
|
+
}
|
|
289
312
|
}
|
|
290
313
|
|
|
291
|
-
const sourceMatches: Match[] = []
|
|
314
|
+
const sourceMatches: Match[] = []
|
|
292
315
|
for (const solidityFile of solidityFiles) {
|
|
293
316
|
if (context.abort.aborted) {
|
|
294
|
-
throw new Error("pattern check aborted")
|
|
317
|
+
throw new Error("pattern check aborted")
|
|
295
318
|
}
|
|
296
|
-
sourceMatches.push(...findMatches(solidityFile, selectedPatterns))
|
|
319
|
+
sourceMatches.push(...findMatches(solidityFile, selectedPatterns))
|
|
297
320
|
}
|
|
298
321
|
|
|
299
322
|
const sources: MatchSource[] = [
|
|
@@ -301,26 +324,42 @@ export async function executePatternCheck(
|
|
|
301
324
|
source: "pattern-db",
|
|
302
325
|
matches: sourceMatches,
|
|
303
326
|
},
|
|
304
|
-
]
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
327
|
+
]
|
|
328
|
+
|
|
329
|
+
if (args.include_scvd === true) {
|
|
330
|
+
try {
|
|
331
|
+
const scvdMatches = await collectScvdMatches(sourceMatches, dependencies)
|
|
332
|
+
if (scvdMatches.length > 0) {
|
|
333
|
+
sources.push({
|
|
334
|
+
source: "scvd",
|
|
335
|
+
matches: scvdMatches,
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
} catch (_e) {
|
|
339
|
+
logger.debug("SCVD enrichment failed, continuing without SCVD matches")
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const allMatches = sources.flatMap((s) => s.matches)
|
|
344
|
+
const bySeverity: Record<string, number> = {}
|
|
345
|
+
const byCategory: Record<string, number> = {}
|
|
346
|
+
for (const m of allMatches) {
|
|
347
|
+
bySeverity[m.severity] = (bySeverity[m.severity] ?? 0) + 1
|
|
348
|
+
if (m.category) {
|
|
349
|
+
byCategory[m.category] = (byCategory[m.category] ?? 0) + 1
|
|
350
|
+
}
|
|
351
|
+
}
|
|
317
352
|
|
|
318
353
|
return {
|
|
354
|
+
success: true,
|
|
355
|
+
matches: allMatches,
|
|
356
|
+
summary: { total: allMatches.length, bySeverity, byCategory },
|
|
319
357
|
sources,
|
|
320
358
|
patternsChecked: selectedPatterns.length,
|
|
321
359
|
executionTime: Date.now() - startedAt,
|
|
322
360
|
target: args.target,
|
|
323
|
-
|
|
361
|
+
patternVersion: PATTERN_PACK_VERSION,
|
|
362
|
+
}
|
|
324
363
|
}
|
|
325
364
|
|
|
326
365
|
export const patternCheckerTool = tool({
|
|
@@ -331,7 +370,7 @@ export const patternCheckerTool = tool({
|
|
|
331
370
|
include_scvd: tool.schema.boolean().default(true),
|
|
332
371
|
},
|
|
333
372
|
async execute(args, context) {
|
|
334
|
-
const result = await executePatternCheck(args, context)
|
|
335
|
-
return JSON.stringify(result)
|
|
373
|
+
const result = await executePatternCheck(args, context)
|
|
374
|
+
return JSON.stringify(result)
|
|
336
375
|
},
|
|
337
|
-
})
|
|
376
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { createLogger } from "../shared/logger"
|
|
4
|
+
import { parseFrontmatter, SkillFrontmatterSchema } from "../skills/skill-schema"
|
|
5
|
+
import type { PatternDefinition } from "./pattern-schema"
|
|
6
|
+
|
|
7
|
+
const logger = createLogger()
|
|
8
|
+
|
|
9
|
+
function listSkillMarkdownFiles(skillsDir: string): string[] {
|
|
10
|
+
if (!existsSync(skillsDir)) {
|
|
11
|
+
logger.warn(`Skills directory does not exist: ${skillsDir}`)
|
|
12
|
+
return []
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const files: string[] = []
|
|
16
|
+
const stack = [skillsDir]
|
|
17
|
+
|
|
18
|
+
while (stack.length > 0) {
|
|
19
|
+
const current = stack.pop()
|
|
20
|
+
if (!current) continue
|
|
21
|
+
|
|
22
|
+
const entries = readdirSync(current, { withFileTypes: true })
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const fullPath = join(current, entry.name)
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
stack.push(fullPath)
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
31
|
+
files.push(fullPath)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return files
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function extractDetectionRulesFromSkills(skillsDir: string): PatternDefinition[] {
|
|
40
|
+
const skillFiles = listSkillMarkdownFiles(skillsDir)
|
|
41
|
+
const extracted: PatternDefinition[] = []
|
|
42
|
+
|
|
43
|
+
for (const filePath of skillFiles) {
|
|
44
|
+
try {
|
|
45
|
+
const content = readFileSync(filePath, "utf-8")
|
|
46
|
+
const frontmatter = parseFrontmatter(content)
|
|
47
|
+
if (!frontmatter) continue
|
|
48
|
+
|
|
49
|
+
const parsed = SkillFrontmatterSchema.safeParse(frontmatter)
|
|
50
|
+
if (!parsed.success) continue
|
|
51
|
+
|
|
52
|
+
const skillName = parsed.data.name
|
|
53
|
+
const category = parsed.data.pattern_category
|
|
54
|
+
if (!category) continue
|
|
55
|
+
|
|
56
|
+
const rules = parsed.data.detection_rules
|
|
57
|
+
if (!rules || rules.length === 0) continue
|
|
58
|
+
|
|
59
|
+
for (const [index, rule] of rules.entries()) {
|
|
60
|
+
extracted.push({
|
|
61
|
+
name: `${skillName}-rule-${index + 1}`,
|
|
62
|
+
category,
|
|
63
|
+
severity: rule.severity,
|
|
64
|
+
confidence: rule.confidence ?? "Medium",
|
|
65
|
+
version: "1.0",
|
|
66
|
+
regex: rule.regex,
|
|
67
|
+
description: rule.description ?? `Detection rule from ${skillName} SKILL.md`,
|
|
68
|
+
...(rule.swc ? { swc: rule.swc } : {}),
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
logger.warn(`Skipping ${filePath}: ${err instanceof Error ? err.message : "parse error"}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return extracted
|
|
77
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canonical pattern category taxonomy.
|
|
5
|
+
* Every builtin, YAML, and skill-derived pattern must belong to one of these.
|
|
6
|
+
*/
|
|
7
|
+
export const PATTERN_CATEGORIES = [
|
|
8
|
+
"reentrancy",
|
|
9
|
+
"oracle-manipulation",
|
|
10
|
+
"flash-loan",
|
|
11
|
+
"access-control",
|
|
12
|
+
"erc4626",
|
|
13
|
+
"proxy",
|
|
14
|
+
"signature",
|
|
15
|
+
"dos",
|
|
16
|
+
"front-running",
|
|
17
|
+
"governance",
|
|
18
|
+
"token-standard",
|
|
19
|
+
"gas-optimization",
|
|
20
|
+
"logic-error",
|
|
21
|
+
"delegatecall",
|
|
22
|
+
] as const
|
|
23
|
+
|
|
24
|
+
export const PatternCategorySchema = z.enum(PATTERN_CATEGORIES)
|
|
25
|
+
|
|
26
|
+
export const PatternDefinitionSchema = z.object({
|
|
27
|
+
name: z.string().min(1).max(128),
|
|
28
|
+
category: PatternCategorySchema,
|
|
29
|
+
severity: z.enum(["Critical", "High", "Medium", "Low", "Informational"]),
|
|
30
|
+
swc: z
|
|
31
|
+
.string()
|
|
32
|
+
.regex(/^SWC-\d+$/)
|
|
33
|
+
.optional(),
|
|
34
|
+
confidence: z.enum(["High", "Medium", "Low"]).default("Medium"),
|
|
35
|
+
version: z.string().default("1.0"),
|
|
36
|
+
regex: z.string().min(1),
|
|
37
|
+
description: z.string().min(1),
|
|
38
|
+
exploit_ref: z.string().url().optional(),
|
|
39
|
+
remediation: z.string().optional(),
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
export type PatternDefinition = z.infer<typeof PatternDefinitionSchema>
|
|
43
|
+
export type PatternCategory = z.infer<typeof PatternCategorySchema>
|
|
44
|
+
|
|
45
|
+
export const PatternPackSchema = z.object({
|
|
46
|
+
pack_name: z.string().optional(),
|
|
47
|
+
pack_version: z.string().default("1.0"),
|
|
48
|
+
patterns: z.array(PatternDefinitionSchema).min(1),
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
export type PatternPack = z.infer<typeof PatternPackSchema>
|