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.
Files changed (178) hide show
  1. package/AGENTS.md +3 -3
  2. package/README.md +229 -13
  3. package/package.json +37 -8
  4. package/skills/INVENTORY.md +88 -57
  5. package/skills/README.md +72 -6
  6. package/skills/case-studies/beanstalk-governance/SKILL.md +52 -0
  7. package/skills/case-studies/bzx-flash-loan/SKILL.md +53 -0
  8. package/skills/case-studies/cream-finance/SKILL.md +52 -0
  9. package/skills/case-studies/curve-reentrancy/SKILL.md +52 -0
  10. package/skills/case-studies/dao-hack/SKILL.md +51 -0
  11. package/skills/case-studies/euler-finance/SKILL.md +52 -0
  12. package/skills/case-studies/harvest-finance/SKILL.md +52 -0
  13. package/skills/case-studies/level-finance/SKILL.md +51 -0
  14. package/skills/case-studies/mango-markets/SKILL.md +53 -0
  15. package/skills/case-studies/nomad-bridge/SKILL.md +51 -0
  16. package/skills/case-studies/parity-multisig/SKILL.md +55 -0
  17. package/skills/case-studies/poly-network/SKILL.md +51 -0
  18. package/skills/case-studies/rari-fuse/SKILL.md +51 -0
  19. package/skills/case-studies/ronin-bridge/SKILL.md +52 -0
  20. package/skills/case-studies/wormhole-bridge/SKILL.md +51 -0
  21. package/skills/checklists/cyfrin-defi-core/SKILL.md +3 -0
  22. package/skills/manifests/cyfrin.json +16 -0
  23. package/skills/manifests/defifofum.json +25 -0
  24. package/skills/manifests/kadenzipfel.json +48 -0
  25. package/skills/manifests/scvd.json +9 -0
  26. package/skills/manifests/smartbugs.json +9 -0
  27. package/skills/manifests/solodit.json +9 -0
  28. package/skills/manifests/sunweb3sec.json +9 -0
  29. package/skills/manifests/trailofbits.json +9 -0
  30. package/skills/methodology/audit-workflow/SKILL.md +3 -0
  31. package/skills/protocol-patterns/amm-dex/SKILL.md +3 -0
  32. package/skills/references/exploit-reference/SKILL.md +3 -0
  33. package/skills/vulnerability-patterns/access-control/SKILL.md +27 -0
  34. package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
  35. package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
  36. package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
  37. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +8 -1
  38. package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
  39. package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
  40. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +8 -1
  41. package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
  42. package/skills/vulnerability-patterns/dos-revert/SKILL.md +14 -1
  43. package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
  44. package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
  45. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +13 -0
  46. package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
  47. package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
  48. package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
  49. package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
  50. package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
  51. package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
  52. package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
  53. package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
  54. package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
  55. package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
  56. package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
  57. package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
  58. package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
  59. package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
  60. package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
  61. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +22 -0
  62. package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
  63. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +11 -1
  64. package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
  65. package/skills/vulnerability-patterns/reentrancy/SKILL.md +22 -0
  66. package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
  67. package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
  68. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +11 -1
  69. package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
  70. package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
  71. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +13 -1
  72. package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
  73. package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
  74. package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
  75. package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
  76. package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
  77. package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
  78. package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
  79. package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
  80. package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
  81. package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
  82. package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
  83. package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
  84. package/src/agents/argus-prompt.ts +27 -10
  85. package/src/agents/pythia-prompt.ts +7 -8
  86. package/src/agents/scribe-prompt.ts +10 -5
  87. package/src/agents/sentinel-prompt.ts +36 -7
  88. package/src/cli/cli-output.ts +16 -0
  89. package/src/cli/cli-program.ts +29 -22
  90. package/src/cli/commands/check-skills.ts +135 -0
  91. package/src/cli/commands/doctor.ts +303 -23
  92. package/src/cli/commands/init.ts +8 -6
  93. package/src/cli/commands/install.ts +10 -8
  94. package/src/cli/commands/lint-skills.ts +118 -0
  95. package/src/cli/index.ts +5 -5
  96. package/src/cli/tui-prompts.ts +4 -2
  97. package/src/cli/types.ts +3 -3
  98. package/src/config/index.ts +1 -1
  99. package/src/config/loader.ts +4 -6
  100. package/src/config/schema.ts +6 -5
  101. package/src/config/types.ts +2 -2
  102. package/src/constants/defaults.ts +2 -0
  103. package/src/create-hooks.ts +225 -29
  104. package/src/create-managers.ts +10 -8
  105. package/src/create-tools.ts +14 -8
  106. package/src/features/background-agent/background-manager.ts +93 -87
  107. package/src/features/background-agent/index.ts +1 -1
  108. package/src/features/context-monitor/context-monitor.ts +3 -3
  109. package/src/features/context-monitor/index.ts +2 -2
  110. package/src/features/error-recovery/session-recovery.ts +2 -4
  111. package/src/features/error-recovery/tool-error-recovery.ts +79 -19
  112. package/src/features/index.ts +5 -5
  113. package/src/features/persistent-state/audit-state-manager.ts +158 -52
  114. package/src/features/persistent-state/global-run-index.ts +38 -0
  115. package/src/features/persistent-state/index.ts +1 -1
  116. package/src/features/persistent-state/run-journal.ts +86 -0
  117. package/src/hooks/agent-tracker.ts +53 -0
  118. package/src/hooks/compaction-hook.ts +46 -37
  119. package/src/hooks/config-handler.ts +31 -11
  120. package/src/hooks/context-budget.ts +42 -0
  121. package/src/hooks/event-hook.ts +48 -23
  122. package/src/hooks/hook-system.ts +4 -4
  123. package/src/hooks/index.ts +5 -5
  124. package/src/hooks/knowledge-sync-hook.ts +19 -21
  125. package/src/hooks/recon-context-builder.ts +66 -0
  126. package/src/hooks/safe-create-hook.ts +9 -11
  127. package/src/hooks/system-prompt-hook.ts +128 -0
  128. package/src/hooks/tool-tracking-hook.ts +162 -29
  129. package/src/hooks/types.ts +2 -1
  130. package/src/index.ts +23 -13
  131. package/src/knowledge/retry.ts +53 -0
  132. package/src/knowledge/scvd-client.ts +103 -83
  133. package/src/knowledge/scvd-errors.ts +89 -0
  134. package/src/knowledge/scvd-index.ts +110 -62
  135. package/src/knowledge/scvd-sync.ts +223 -47
  136. package/src/knowledge/source-manifest.ts +102 -0
  137. package/src/managers/index.ts +1 -1
  138. package/src/managers/types.ts +19 -14
  139. package/src/plugin-interface.ts +19 -8
  140. package/src/shared/binary-utils.ts +44 -34
  141. package/src/shared/deep-merge.ts +55 -36
  142. package/src/shared/file-utils.ts +21 -19
  143. package/src/shared/index.ts +11 -5
  144. package/src/shared/jsonc-parser.ts +123 -28
  145. package/src/shared/logger.ts +91 -17
  146. package/src/shared/project-utils.ts +30 -0
  147. package/src/skills/analysis/cluster.ts +414 -0
  148. package/src/skills/analysis/gates.ts +227 -0
  149. package/src/skills/analysis/index.ts +33 -0
  150. package/src/skills/analysis/normalize.ts +217 -0
  151. package/src/skills/analysis/similarity.ts +224 -0
  152. package/src/skills/argus-skill-resolver.ts +237 -0
  153. package/src/skills/skill-schema.ts +99 -0
  154. package/src/solodit-lifecycle.ts +202 -0
  155. package/src/state/audit-state.ts +10 -8
  156. package/src/state/finding-store.ts +68 -55
  157. package/src/state/types.ts +96 -44
  158. package/src/tools/argus-skill-load-tool.ts +78 -0
  159. package/src/tools/contract-analyzer-tool.ts +60 -77
  160. package/src/tools/forge-coverage-tool.ts +226 -0
  161. package/src/tools/forge-fuzz-tool.ts +127 -127
  162. package/src/tools/forge-test-tool.ts +153 -157
  163. package/src/tools/gas-analysis-tool.ts +264 -0
  164. package/src/tools/pattern-checker-tool.ts +206 -167
  165. package/src/tools/pattern-loader.ts +77 -0
  166. package/src/tools/pattern-schema.ts +51 -0
  167. package/src/tools/proxy-detection-tool.ts +224 -0
  168. package/src/tools/report-generator-tool.ts +333 -142
  169. package/src/tools/slither-tool.ts +300 -210
  170. package/src/tools/solodit-search-tool.ts +255 -80
  171. package/src/tools/sync-knowledge-tool.ts +7 -11
  172. package/src/utils/audit-artifact-detector.ts +118 -0
  173. package/src/utils/dependency-scanner.ts +93 -0
  174. package/src/utils/project-detector.ts +175 -86
  175. package/src/utils/solidity-parser.ts +112 -67
  176. package/src/utils/solodit-health.ts +29 -0
  177. package/src/hooks/event-hook-v2.ts +0 -99
  178. package/src/state/plugin-state.ts +0 -14
@@ -1,96 +1,78 @@
1
- import os from "node:os";
2
- import { readdirSync, readFileSync, statSync } from "node:fs";
3
- import { extname, join, resolve } from "node:path";
4
- import { tool, type ToolContext } from "@opencode-ai/plugin";
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
- } from "../knowledge/scvd-index";
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
- sources: MatchSource[];
28
- patternsChecked: number;
29
- executionTime: number;
30
- target: string;
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 BuiltinPattern = {
48
- name: string;
49
- category: string;
50
- severity: Match["severity"];
51
- regex: RegExp;
52
- description: string;
53
- exploitReference?: string;
54
- };
55
-
56
- const BUILTIN_PATTERNS: BuiltinPattern[] = [
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
- const PATTERN_NAME_TO_CATEGORY = new Map(
105
- BUILTIN_PATTERNS.map((pattern) => [pattern.name, pattern.category])
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 = PATTERN_NAME_TO_CATEGORY.get(match.pattern);
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
- throw new Error(`Target does not exist: ${target}`);
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: BuiltinPattern[]): Match[] {
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(pattern.regex.source, pattern.regex.flags.includes("g") ? pattern.regex.flags : `${pattern.regex.flags}g`);
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(categories?: string[]): BuiltinPattern[] {
265
+ function selectPatterns(
266
+ availablePatterns: LoadedPattern[],
267
+ categories?: string[],
268
+ ): LoadedPattern[] {
263
269
  if (!categories || categories.length === 0) {
264
- return BUILTIN_PATTERNS;
270
+ return availablePatterns
265
271
  }
266
272
 
267
- const set = new Set(categories);
268
- return BUILTIN_PATTERNS.filter((pattern) => set.has(pattern.category));
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 startedAt = Date.now();
283
- context.metadata({ title: `Pattern check: ${args.target}` });
291
+ const skillsDir = join(dirname(dirname(__dirname)), "skills")
292
+ const skillDetectionRules = extractDetectionRulesFromSkills(skillsDir)
284
293
 
285
- const selectedPatterns = selectPatterns(args.patterns);
286
- const solidityFiles = collectSolidityFiles(args.target);
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
- throw new Error(`No Solidity files found for target: ${args.target}`);
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
- if (args.include_scvd === true) {
307
- try {
308
- const scvdMatches = await collectScvdMatches(sourceMatches, dependencies);
309
- if (scvdMatches.length > 0) {
310
- sources.push({
311
- source: "scvd",
312
- matches: scvdMatches,
313
- });
314
- }
315
- } catch (_e) { /* non-critical: SCVD enrichment is best-effort */ }
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>