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.
Files changed (169) hide show
  1. package/AGENTS.md +3 -3
  2. package/README.md +93 -37
  3. package/package.json +34 -7
  4. package/skills/INVENTORY.md +88 -57
  5. package/skills/README.md +26 -23
  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/manifests/smartbugs.json +1 -3
  22. package/skills/manifests/sunweb3sec.json +1 -3
  23. package/skills/vulnerability-patterns/access-control/SKILL.md +14 -0
  24. package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
  25. package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
  26. package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
  27. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +2 -1
  28. package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
  29. package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
  30. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +2 -1
  31. package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
  32. package/skills/vulnerability-patterns/dos-revert/SKILL.md +1 -0
  33. package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
  34. package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
  35. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +1 -0
  36. package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
  37. package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
  38. package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
  39. package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
  40. package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
  41. package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
  42. package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
  43. package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
  44. package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
  45. package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
  46. package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
  47. package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
  48. package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
  49. package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
  50. package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
  51. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +9 -0
  52. package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
  53. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +1 -0
  54. package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
  55. package/skills/vulnerability-patterns/reentrancy/SKILL.md +9 -0
  56. package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
  57. package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
  58. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +2 -1
  59. package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
  60. package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
  61. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +2 -1
  62. package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
  63. package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
  64. package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
  65. package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
  66. package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
  67. package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
  68. package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
  69. package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
  70. package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
  71. package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
  72. package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
  73. package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
  74. package/src/agents/argus-prompt.ts +34 -7
  75. package/src/agents/pythia-prompt.ts +13 -4
  76. package/src/agents/scribe-prompt.ts +20 -2
  77. package/src/agents/sentinel-prompt.ts +45 -5
  78. package/src/cli/cli-program.ts +29 -26
  79. package/src/cli/commands/check-skills.ts +135 -0
  80. package/src/cli/commands/doctor.ts +48 -26
  81. package/src/cli/commands/init.ts +5 -3
  82. package/src/cli/commands/install.ts +7 -5
  83. package/src/cli/commands/lint-skills.ts +16 -12
  84. package/src/cli/index.ts +5 -5
  85. package/src/cli/types.ts +3 -3
  86. package/src/config/index.ts +1 -1
  87. package/src/config/loader.ts +4 -6
  88. package/src/config/schema.ts +6 -5
  89. package/src/config/types.ts +2 -2
  90. package/src/constants/defaults.ts +2 -0
  91. package/src/create-hooks.ts +145 -34
  92. package/src/create-managers.ts +10 -8
  93. package/src/create-tools.ts +13 -9
  94. package/src/features/background-agent/background-manager.ts +93 -87
  95. package/src/features/background-agent/index.ts +1 -1
  96. package/src/features/context-monitor/context-monitor.ts +3 -3
  97. package/src/features/context-monitor/index.ts +2 -2
  98. package/src/features/error-recovery/session-recovery.ts +2 -4
  99. package/src/features/error-recovery/tool-error-recovery.ts +12 -7
  100. package/src/features/index.ts +5 -5
  101. package/src/features/persistent-state/audit-state-manager.ts +143 -60
  102. package/src/features/persistent-state/global-run-index.ts +38 -0
  103. package/src/features/persistent-state/index.ts +1 -1
  104. package/src/features/persistent-state/run-journal.ts +86 -0
  105. package/src/hooks/config-handler.ts +28 -11
  106. package/src/hooks/context-budget.ts +2 -5
  107. package/src/hooks/event-hook.ts +47 -23
  108. package/src/hooks/hook-system.ts +4 -4
  109. package/src/hooks/index.ts +5 -5
  110. package/src/hooks/knowledge-sync-hook.ts +18 -21
  111. package/src/hooks/recon-context-builder.ts +2 -2
  112. package/src/hooks/safe-create-hook.ts +6 -7
  113. package/src/hooks/system-prompt-hook.ts +18 -1
  114. package/src/hooks/tool-tracking-hook.ts +110 -51
  115. package/src/hooks/types.ts +2 -1
  116. package/src/index.ts +24 -37
  117. package/src/knowledge/retry.ts +22 -22
  118. package/src/knowledge/scvd-client.ts +88 -95
  119. package/src/knowledge/scvd-errors.ts +35 -35
  120. package/src/knowledge/scvd-index.ts +78 -80
  121. package/src/knowledge/scvd-sync.ts +106 -101
  122. package/src/managers/index.ts +1 -1
  123. package/src/managers/types.ts +19 -14
  124. package/src/plugin-interface.ts +7 -9
  125. package/src/shared/binary-utils.ts +44 -35
  126. package/src/shared/deep-merge.ts +55 -36
  127. package/src/shared/file-utils.ts +21 -19
  128. package/src/shared/index.ts +11 -5
  129. package/src/shared/jsonc-parser.ts +123 -28
  130. package/src/shared/logger.ts +16 -3
  131. package/src/shared/project-utils.ts +30 -0
  132. package/src/skills/analysis/cluster.ts +414 -0
  133. package/src/skills/analysis/gates.ts +227 -0
  134. package/src/skills/analysis/index.ts +33 -0
  135. package/src/skills/analysis/normalize.ts +217 -0
  136. package/src/skills/analysis/similarity.ts +224 -0
  137. package/src/skills/argus-skill-resolver.ts +17 -6
  138. package/src/skills/skill-schema.ts +11 -10
  139. package/src/solodit-lifecycle.ts +203 -0
  140. package/src/state/audit-state.ts +8 -8
  141. package/src/state/finding-store.ts +68 -55
  142. package/src/state/types.ts +88 -67
  143. package/src/tools/argus-skill-load-tool.ts +12 -7
  144. package/src/tools/contract-analyzer-tool.ts +142 -77
  145. package/src/tools/forge-coverage-tool.ts +226 -0
  146. package/src/tools/forge-fuzz-tool.ts +127 -127
  147. package/src/tools/forge-test-tool.ts +201 -158
  148. package/src/tools/gas-analysis-tool.ts +264 -0
  149. package/src/tools/pattern-checker-tool.ts +203 -191
  150. package/src/tools/pattern-loader.ts +5 -111
  151. package/src/tools/pattern-schema.ts +3 -0
  152. package/src/tools/proxy-detection-tool.ts +224 -0
  153. package/src/tools/report-generator-tool.ts +305 -206
  154. package/src/tools/slither-tool.ts +266 -218
  155. package/src/tools/solodit-search-tool.ts +235 -119
  156. package/src/tools/sync-knowledge-tool.ts +7 -11
  157. package/src/utils/audit-artifact-detector.ts +28 -29
  158. package/src/utils/dependency-scanner.ts +37 -37
  159. package/src/utils/project-detector.ts +111 -124
  160. package/src/utils/solidity-parser.ts +175 -75
  161. package/skills/patterns/access-control.yaml +0 -31
  162. package/skills/patterns/erc4626.yaml +0 -29
  163. package/skills/patterns/flash-loan.yaml +0 -20
  164. package/skills/patterns/oracle.yaml +0 -30
  165. package/skills/patterns/proxy.yaml +0 -30
  166. package/skills/patterns/reentrancy.yaml +0 -30
  167. package/skills/patterns/signature.yaml +0 -31
  168. package/src/hooks/event-hook-v2.ts +0 -99
  169. package/src/state/plugin-state.ts +0 -14
@@ -1,109 +1,81 @@
1
- import os from "node:os";
2
- import { readdirSync, readFileSync, statSync } from "node:fs";
3
- import { dirname, 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";
11
- import {
12
- extractDetectionRulesFromSkills,
13
- loadPatternPacks,
14
- } from "./pattern-loader";
15
- import type { PatternDefinition } from "./pattern-schema";
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
16
 
17
- export type PatternSource = "builtin" | "yaml" | "skill";
17
+ export type PatternSource = "skill"
18
18
 
19
19
  export interface Match {
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;
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
28
28
  }
29
29
 
30
30
  export interface MatchSource {
31
- source: string;
32
- matches: Match[];
31
+ source: string
32
+ matches: Match[]
33
33
  }
34
34
 
35
35
  export interface PatternCheckResult {
36
- sources: MatchSource[];
37
- patternsChecked: number;
38
- executionTime: number;
39
- target: string;
40
- patternVersion?: 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
41
49
  }
42
50
 
43
51
  type PatternCheckArgs = {
44
- target: string;
45
- patterns?: string[];
46
- include_scvd?: boolean;
47
- };
52
+ target: string
53
+ patterns?: string[]
54
+ include_scvd?: boolean
55
+ }
48
56
 
49
57
  type PatternCheckDependencies = {
50
- loadIndexFn?: (filePath: string) => Promise<ScvdIndex | null>;
58
+ loadIndexFn?: (filePath: string) => Promise<ScvdIndex | null>
51
59
  searchIndexFn?: (
52
60
  index: ScvdIndex,
53
- query: { swc?: string; severity?: string; keyword?: string; limit?: number }
54
- ) => ScvdIndexEntry[];
55
- };
56
-
57
- type BuiltinPattern = {
58
- name: string;
59
- category: string;
60
- severity: Match["severity"];
61
- regex: RegExp;
62
- description: string;
63
- exploitReference?: string;
64
- source?: PatternSource;
65
- };
66
-
67
- export const PATTERN_PACK_VERSION = "1.0.0";
68
-
69
- const BUILTIN_PATTERNS: BuiltinPattern[] = [
70
- {
71
- name: "reentrancy",
72
- category: "reentrancy",
73
- severity: "High",
74
- regex: /\.call\{value:/,
75
- description: "Potential reentrancy: ETH transfer via low-level call",
76
- exploitReference: "DAO hack ($60M), 2016",
77
- },
78
- {
79
- name: "tx-origin-auth",
80
- category: "access-control",
81
- severity: "High",
82
- regex: /tx\.origin/,
83
- description: "Use of tx.origin for authorization - vulnerable to phishing",
84
- },
85
- {
86
- name: "selfdestruct",
87
- category: "access-control",
88
- severity: "High",
89
- regex: /selfdestruct\(|suicide\(/,
90
- description: "Contract uses selfdestruct - can destroy contract",
91
- },
92
- {
93
- name: "delegatecall",
94
- category: "delegatecall",
95
- severity: "High",
96
- regex: /\.delegatecall\(/,
97
- description: "Use of delegatecall - can overwrite storage",
98
- },
99
- {
100
- name: "missing-zero-check",
101
- category: "access-control",
102
- severity: "Medium",
103
- regex: /address\(0\)/,
104
- description: "Potential missing zero-address validation",
105
- },
106
- ];
61
+ query: { swc?: string; severity?: string; keyword?: string; limit?: number },
62
+ ) => ScvdIndexEntry[]
63
+ }
64
+
65
+ export 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
+ confidence?: "High" | "Medium" | "Low"
74
+ applies_to?: string[]
75
+ exclude_if?: string[]
76
+ }
77
+
78
+ export const PATTERN_PACK_VERSION = "1.0.0"
107
79
 
108
80
  const CATEGORY_TO_SWC: Record<string, string[]> = {
109
81
  reentrancy: ["SWC-107"],
@@ -112,24 +84,25 @@ const CATEGORY_TO_SWC: Record<string, string[]> = {
112
84
  delegatecall: ["SWC-112"],
113
85
  "signature-replay": ["SWC-121"],
114
86
  "integer-overflow": ["SWC-101"],
115
- };
116
-
117
- const PATTERN_NAME_TO_CATEGORY = new Map(
118
- BUILTIN_PATTERNS.map((pattern) => [pattern.name, pattern.category])
119
- );
87
+ governance: ["SWC-105", "SWC-106"],
88
+ "front-running": ["SWC-114"],
89
+ "logic-error": ["SWC-101", "SWC-116"],
90
+ "gas-optimization": ["SWC-128"],
91
+ dos: ["SWC-128"],
92
+ }
120
93
 
121
94
  function normalizeSeverity(value: string): Match["severity"] {
122
- if (value === "Critical") return "Critical";
123
- if (value === "High") return "High";
124
- if (value === "Medium") return "Medium";
125
- if (value === "Low") return "Low";
126
- return "Informational";
95
+ if (value === "Critical") return "Critical"
96
+ if (value === "High") return "High"
97
+ if (value === "Medium") return "Medium"
98
+ if (value === "Low") return "Low"
99
+ return "Informational"
127
100
  }
128
101
 
129
102
  function normalizePatternDefinitions(
130
103
  patterns: PatternDefinition[],
131
- source: PatternSource
132
- ): BuiltinPattern[] {
104
+ source: PatternSource,
105
+ ): LoadedPattern[] {
133
106
  return patterns.map((patternDef) => ({
134
107
  name: patternDef.name,
135
108
  category: patternDef.category,
@@ -137,56 +110,59 @@ function normalizePatternDefinitions(
137
110
  regex: new RegExp(patternDef.regex),
138
111
  description: patternDef.description,
139
112
  ...(patternDef.exploit_ref ? { exploitReference: patternDef.exploit_ref } : {}),
113
+ ...(patternDef.confidence ? { confidence: patternDef.confidence } : {}),
114
+ ...(patternDef.applies_to ? { applies_to: patternDef.applies_to } : {}),
115
+ ...(patternDef.exclude_if ? { exclude_if: patternDef.exclude_if } : {}),
140
116
  source,
141
- }));
117
+ }))
142
118
  }
143
119
 
144
120
  function uniqueScvdEntries(entries: ScvdIndexEntry[]): ScvdIndexEntry[] {
145
- const deduped = new Map<string, ScvdIndexEntry>();
121
+ const deduped = new Map<string, ScvdIndexEntry>()
146
122
  for (const entry of entries) {
147
- deduped.set(entry.id, entry);
123
+ deduped.set(entry.id, entry)
148
124
  }
149
- return Array.from(deduped.values());
125
+ return Array.from(deduped.values())
150
126
  }
151
127
 
152
128
  async function collectScvdMatches(
153
129
  matches: Match[],
154
- dependencies: Required<PatternCheckDependencies>
130
+ dependencies: Required<PatternCheckDependencies>,
155
131
  ): Promise<Match[]> {
156
- const detectedCategories = new Set<string>();
132
+ const detectedCategories = new Set<string>()
157
133
  for (const match of matches) {
158
- const category = match.category ?? PATTERN_NAME_TO_CATEGORY.get(match.pattern);
134
+ const category = match.category
159
135
  if (category) {
160
- detectedCategories.add(category);
136
+ detectedCategories.add(category)
161
137
  }
162
138
  }
163
139
 
164
140
  if (detectedCategories.size === 0) {
165
- return [];
141
+ return []
166
142
  }
167
143
 
168
- const swcCodes = new Set<string>();
144
+ const swcCodes = new Set<string>()
169
145
  for (const category of detectedCategories) {
170
- const mappedSwcs = CATEGORY_TO_SWC[category] ?? [];
146
+ const mappedSwcs = CATEGORY_TO_SWC[category] ?? []
171
147
  for (const swcCode of mappedSwcs) {
172
- swcCodes.add(swcCode);
148
+ swcCodes.add(swcCode)
173
149
  }
174
150
  }
175
151
 
176
152
  if (swcCodes.size === 0) {
177
- return [];
153
+ return []
178
154
  }
179
155
 
180
- const indexPath = join(os.homedir(), ".cache", "solidity-argus", "scvd-index.json");
181
- const index = await dependencies.loadIndexFn(indexPath);
156
+ const indexPath = join(os.homedir(), ".cache", "solidity-argus", "scvd-index.json")
157
+ const index = await dependencies.loadIndexFn(indexPath)
182
158
 
183
159
  if (!index) {
184
- return [];
160
+ return []
185
161
  }
186
162
 
187
- const entries: ScvdIndexEntry[] = [];
163
+ const entries: ScvdIndexEntry[] = []
188
164
  for (const swcCode of swcCodes) {
189
- entries.push(...dependencies.searchIndexFn(index, { swc: swcCode }));
165
+ entries.push(...dependencies.searchIndexFn(index, { swc: swcCode }))
190
166
  }
191
167
 
192
168
  return uniqueScvdEntries(entries).map((entry) => ({
@@ -196,83 +172,97 @@ async function collectScvdMatches(
196
172
  lines: [1, 1],
197
173
  description: entry.title,
198
174
  exploitReference: entry.repoUrl,
199
- }));
175
+ }))
200
176
  }
201
177
 
202
178
  function collectSolidityFiles(target: string, maxDepth = 8): string[] {
203
- const absoluteTarget = resolve(target);
204
- let stats: ReturnType<typeof statSync>;
179
+ const absoluteTarget = resolve(target)
180
+ let stats: ReturnType<typeof statSync>
205
181
 
206
182
  try {
207
- stats = statSync(absoluteTarget);
183
+ stats = statSync(absoluteTarget)
208
184
  } catch {
209
- throw new Error(`Target does not exist: ${target}`);
185
+ return []
210
186
  }
211
187
 
212
188
  if (stats.isFile()) {
213
- return extname(absoluteTarget) === ".sol" ? [absoluteTarget] : [];
189
+ return extname(absoluteTarget) === ".sol" ? [absoluteTarget] : []
214
190
  }
215
191
 
216
192
  if (!stats.isDirectory()) {
217
- return [];
193
+ return []
218
194
  }
219
195
 
220
- const discovered: string[] = [];
221
- const stack: Array<{ path: string; depth: number }> = [{ path: absoluteTarget, depth: 0 }];
196
+ const discovered: string[] = []
197
+ const stack: Array<{ path: string; depth: number }> = [{ path: absoluteTarget, depth: 0 }]
222
198
 
223
199
  while (stack.length > 0) {
224
- const current = stack.pop();
200
+ const current = stack.pop()
225
201
  if (!current || current.depth > maxDepth) {
226
- continue;
202
+ continue
227
203
  }
228
204
 
229
- const entries = readdirSync(current.path, { withFileTypes: true });
205
+ const entries = readdirSync(current.path, { withFileTypes: true })
230
206
  for (const entry of entries) {
231
- const fullPath = resolve(current.path, entry.name);
207
+ const fullPath = resolve(current.path, entry.name)
232
208
  if (entry.isDirectory()) {
233
- stack.push({ path: fullPath, depth: current.depth + 1 });
234
- continue;
209
+ stack.push({ path: fullPath, depth: current.depth + 1 })
210
+ continue
235
211
  }
236
212
 
237
213
  if (entry.isFile() && extname(entry.name) === ".sol") {
238
- discovered.push(fullPath);
214
+ discovered.push(fullPath)
239
215
  }
240
216
  }
241
217
  }
242
218
 
243
- return discovered;
219
+ return discovered
244
220
  }
245
221
 
246
222
  function lineNumberAt(content: string, index: number): number {
247
223
  if (index <= 0) {
248
- return 1;
224
+ return 1
249
225
  }
250
226
 
251
- let line = 1;
227
+ let line = 1
252
228
  for (let i = 0; i < index && i < content.length; i += 1) {
253
229
  if (content[i] === "\n") {
254
- line += 1;
230
+ line += 1
255
231
  }
256
232
  }
257
- return line;
233
+ return line
258
234
  }
259
235
 
260
236
  function lineWindow(content: string, index: number): [number, number] {
261
- const linesCount = content.split("\n").length;
262
- const line = lineNumberAt(content, index);
263
- const start = Math.max(1, line - 5);
264
- const end = Math.min(linesCount, line + 5);
265
- return [start, end];
237
+ const linesCount = content.split("\n").length
238
+ const line = lineNumberAt(content, index)
239
+ const start = Math.max(1, line - 5)
240
+ const end = Math.min(linesCount, line + 5)
241
+ return [start, end]
266
242
  }
267
243
 
268
- function findMatches(file: string, patterns: BuiltinPattern[]): Match[] {
269
- const content = readFileSync(file, "utf8");
270
- const matches: Match[] = [];
244
+ export function findMatches(file: string, patterns: LoadedPattern[]): Match[] {
245
+ const content = readFileSync(file, "utf8")
246
+ const matches: Match[] = []
247
+
248
+ // Strip comments and string literals to reduce false positives.
249
+ // Use a space-preserving approach so line numbers remain valid.
250
+ // Order: multi-line comments first (can contain //), then single-line, then strings.
251
+ const stripped = content
252
+ .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "))
253
+ .replace(/\/\/[^\n]*/g, (m) => " ".repeat(m.length))
254
+ .replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, (m) => {
255
+ const quote = m[0]
256
+ return `${quote}${" ".repeat(Math.max(0, m.length - 2))}${quote}`
257
+ })
271
258
 
272
259
  for (const pattern of patterns) {
273
- const regex = new RegExp(pattern.regex.source, pattern.regex.flags.includes("g") ? pattern.regex.flags : `${pattern.regex.flags}g`);
274
- for (const found of content.matchAll(regex)) {
275
- const index = found.index ?? 0;
260
+ const regex = new RegExp(
261
+ pattern.regex.source,
262
+ pattern.regex.flags.includes("g") ? pattern.regex.flags : `${pattern.regex.flags}g`,
263
+ )
264
+ for (const found of stripped.matchAll(regex)) {
265
+ const index = found.index ?? 0
276
266
  matches.push({
277
267
  pattern: pattern.name,
278
268
  severity: pattern.severity,
@@ -280,63 +270,70 @@ function findMatches(file: string, patterns: BuiltinPattern[]): Match[] {
280
270
  lines: lineWindow(content, index),
281
271
  description: pattern.description,
282
272
  exploitReference: pattern.exploitReference,
283
- patternSource: pattern.source ?? "builtin",
273
+ patternSource: pattern.source ?? "skill",
284
274
  category: pattern.category,
285
- });
275
+ })
286
276
  }
287
277
  }
288
278
 
289
- return matches;
279
+ return matches
290
280
  }
291
281
 
292
282
  function selectPatterns(
293
- availablePatterns: BuiltinPattern[],
294
- categories?: string[]
295
- ): BuiltinPattern[] {
283
+ availablePatterns: LoadedPattern[],
284
+ categories?: string[],
285
+ ): LoadedPattern[] {
296
286
  if (!categories || categories.length === 0) {
297
- return availablePatterns;
287
+ return availablePatterns
298
288
  }
299
289
 
300
- const set = new Set(categories);
301
- return availablePatterns.filter((pattern) => set.has(pattern.category));
290
+ const set = new Set(categories)
291
+ return availablePatterns.filter((pattern) => set.has(pattern.category))
302
292
  }
303
293
 
304
294
  export async function executePatternCheck(
305
295
  args: PatternCheckArgs,
306
296
  context: ToolContext,
307
- deps: PatternCheckDependencies = {}
297
+ deps: PatternCheckDependencies = {},
308
298
  ): Promise<PatternCheckResult> {
309
299
  const dependencies: Required<PatternCheckDependencies> = {
310
300
  loadIndexFn: loadIndex,
311
301
  searchIndexFn: searchIndex,
312
302
  ...deps,
313
- };
303
+ }
314
304
 
315
- const startedAt = Date.now();
316
- context.metadata({ title: `Pattern check: ${args.target}` });
305
+ const startedAt = Date.now()
306
+ context.metadata({ title: `Pattern check: ${args.target}` })
317
307
 
318
- const skillsDir = join(dirname(dirname(__dirname)), "skills");
319
- const yamlPatterns = loadPatternPacks(join(skillsDir, "patterns"));
320
- const skillDetectionRules = extractDetectionRulesFromSkills(skillsDir);
308
+ const skillsDir = join(dirname(dirname(__dirname)), "skills")
309
+ const skillDetectionRules = extractDetectionRulesFromSkills(skillsDir)
321
310
 
322
- const allPatterns: BuiltinPattern[] = [
323
- ...BUILTIN_PATTERNS.map((pattern) => ({ ...pattern, source: "builtin" as const })),
324
- ...normalizePatternDefinitions(yamlPatterns, "yaml"),
311
+ const allPatterns: LoadedPattern[] = [
325
312
  ...normalizePatternDefinitions(skillDetectionRules, "skill"),
326
- ];
313
+ ]
327
314
 
328
- const selectedPatterns = selectPatterns(allPatterns, args.patterns);
329
- const solidityFiles = collectSolidityFiles(args.target);
315
+ const selectedPatterns = selectPatterns(allPatterns, args.patterns)
316
+ const solidityFiles = collectSolidityFiles(args.target)
330
317
  if (solidityFiles.length === 0) {
331
- throw new Error(`No Solidity files found for target: ${args.target}`);
318
+ return {
319
+ success: false,
320
+ error: `No Solidity files found for target: ${args.target}`,
321
+ matches: [],
322
+ summary: { total: 0, bySeverity: {}, byCategory: {} },
323
+ sources: [],
324
+ patternsChecked: selectedPatterns.length,
325
+ executionTime: Date.now() - startedAt,
326
+ target: args.target,
327
+ patternVersion: PATTERN_PACK_VERSION,
328
+ }
332
329
  }
333
330
 
334
- const sourceMatches: Match[] = [];
331
+ const sourceMatches: Match[] = []
335
332
  for (const solidityFile of solidityFiles) {
336
333
  if (context.abort.aborted) {
337
- throw new Error("pattern check aborted");
334
+ throw new Error("pattern check aborted")
338
335
  }
339
- sourceMatches.push(...findMatches(solidityFile, selectedPatterns));
336
+ sourceMatches.push(...findMatches(solidityFile, selectedPatterns))
340
337
  }
341
338
 
342
339
  const sources: MatchSource[] = [
@@ -344,27 +341,42 @@ export async function executePatternCheck(
344
341
  source: "pattern-db",
345
342
  matches: sourceMatches,
346
343
  },
347
- ];
348
-
349
- if (args.include_scvd === true) {
350
- try {
351
- const scvdMatches = await collectScvdMatches(sourceMatches, dependencies);
352
- if (scvdMatches.length > 0) {
353
- sources.push({
354
- source: "scvd",
355
- matches: scvdMatches,
356
- });
357
- }
358
- } catch (_e) { /* non-critical: SCVD enrichment is best-effort */ }
359
- }
344
+ ]
345
+
346
+ if (args.include_scvd === true) {
347
+ try {
348
+ const scvdMatches = await collectScvdMatches(sourceMatches, dependencies)
349
+ if (scvdMatches.length > 0) {
350
+ sources.push({
351
+ source: "scvd",
352
+ matches: scvdMatches,
353
+ })
354
+ }
355
+ } catch (_e) {
356
+ logger.debug("SCVD enrichment failed, continuing without SCVD matches")
357
+ }
358
+ }
359
+
360
+ const allMatches = sources.flatMap((s) => s.matches)
361
+ const bySeverity: Record<string, number> = {}
362
+ const byCategory: Record<string, number> = {}
363
+ for (const m of allMatches) {
364
+ bySeverity[m.severity] = (bySeverity[m.severity] ?? 0) + 1
365
+ if (m.category) {
366
+ byCategory[m.category] = (byCategory[m.category] ?? 0) + 1
367
+ }
368
+ }
360
369
 
361
370
  return {
371
+ success: true,
372
+ matches: allMatches,
373
+ summary: { total: allMatches.length, bySeverity, byCategory },
362
374
  sources,
363
375
  patternsChecked: selectedPatterns.length,
364
376
  executionTime: Date.now() - startedAt,
365
377
  target: args.target,
366
378
  patternVersion: PATTERN_PACK_VERSION,
367
- };
379
+ }
368
380
  }
369
381
 
370
382
  export const patternCheckerTool = tool({
@@ -375,7 +387,7 @@ export const patternCheckerTool = tool({
375
387
  include_scvd: tool.schema.boolean().default(true),
376
388
  },
377
389
  async execute(args, context) {
378
- const result = await executePatternCheck(args, context);
379
- return JSON.stringify(result);
390
+ const result = await executePatternCheck(args, context)
391
+ return JSON.stringify(result)
380
392
  },
381
- });
393
+ })