solidity-argus 0.1.8 → 0.2.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 (84) hide show
  1. package/README.md +161 -1
  2. package/package.json +5 -2
  3. package/skills/README.md +63 -0
  4. package/skills/checklists/cyfrin-defi-core/SKILL.md +3 -0
  5. package/skills/manifests/cyfrin.json +16 -0
  6. package/skills/manifests/defifofum.json +25 -0
  7. package/skills/manifests/kadenzipfel.json +48 -0
  8. package/skills/manifests/scvd.json +9 -0
  9. package/skills/manifests/smartbugs.json +11 -0
  10. package/skills/manifests/solodit.json +9 -0
  11. package/skills/manifests/sunweb3sec.json +11 -0
  12. package/skills/manifests/trailofbits.json +9 -0
  13. package/skills/methodology/audit-workflow/SKILL.md +3 -0
  14. package/skills/patterns/access-control.yaml +31 -0
  15. package/skills/patterns/erc4626.yaml +29 -0
  16. package/skills/patterns/flash-loan.yaml +20 -0
  17. package/skills/patterns/oracle.yaml +30 -0
  18. package/skills/patterns/proxy.yaml +30 -0
  19. package/skills/patterns/reentrancy.yaml +30 -0
  20. package/skills/patterns/signature.yaml +31 -0
  21. package/skills/protocol-patterns/amm-dex/SKILL.md +3 -0
  22. package/skills/references/exploit-reference/SKILL.md +3 -0
  23. package/skills/vulnerability-patterns/access-control/SKILL.md +13 -0
  24. package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +6 -0
  25. package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +6 -0
  26. package/skills/vulnerability-patterns/dos-revert/SKILL.md +13 -1
  27. package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +12 -0
  28. package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +13 -0
  29. package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +10 -1
  30. package/skills/vulnerability-patterns/reentrancy/SKILL.md +13 -0
  31. package/skills/vulnerability-patterns/signature-malleability/SKILL.md +9 -0
  32. package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +11 -0
  33. package/src/agents/argus-prompt.ts +4 -4
  34. package/src/agents/pythia-prompt.ts +4 -4
  35. package/src/agents/scribe-prompt.ts +3 -3
  36. package/src/agents/sentinel-prompt.ts +4 -4
  37. package/src/cli/cli-output.ts +16 -0
  38. package/src/cli/cli-program.ts +9 -5
  39. package/src/cli/commands/doctor.ts +274 -16
  40. package/src/cli/commands/init.ts +5 -5
  41. package/src/cli/commands/install.ts +5 -5
  42. package/src/cli/commands/lint-skills.ts +114 -0
  43. package/src/cli/tui-prompts.ts +4 -2
  44. package/src/config/schema.ts +2 -0
  45. package/src/create-hooks.ts +99 -14
  46. package/src/create-tools.ts +2 -0
  47. package/src/features/error-recovery/tool-error-recovery.ts +74 -19
  48. package/src/features/persistent-state/audit-state-manager.ts +36 -13
  49. package/src/hooks/agent-tracker.ts +53 -0
  50. package/src/hooks/compaction-hook.ts +46 -37
  51. package/src/hooks/config-handler.ts +3 -0
  52. package/src/hooks/context-budget.ts +45 -0
  53. package/src/hooks/event-hook.ts +5 -4
  54. package/src/hooks/knowledge-sync-hook.ts +2 -1
  55. package/src/hooks/recon-context-builder.ts +66 -0
  56. package/src/hooks/safe-create-hook.ts +4 -5
  57. package/src/hooks/system-prompt-hook.ts +128 -0
  58. package/src/hooks/tool-tracking-hook.ts +86 -7
  59. package/src/index.ts +24 -1
  60. package/src/knowledge/retry.ts +53 -0
  61. package/src/knowledge/scvd-client.ts +37 -10
  62. package/src/knowledge/scvd-errors.ts +89 -0
  63. package/src/knowledge/scvd-index.ts +53 -3
  64. package/src/knowledge/scvd-sync.ts +205 -34
  65. package/src/knowledge/source-manifest.ts +102 -0
  66. package/src/plugin-interface.ts +14 -1
  67. package/src/shared/binary-utils.ts +1 -0
  68. package/src/shared/logger.ts +78 -17
  69. package/src/skills/argus-skill-resolver.ts +226 -0
  70. package/src/skills/skill-schema.ts +98 -0
  71. package/src/state/audit-state.ts +2 -0
  72. package/src/state/types.ts +32 -1
  73. package/src/tools/argus-skill-load-tool.ts +73 -0
  74. package/src/tools/pattern-checker-tool.ts +56 -12
  75. package/src/tools/pattern-loader.ts +183 -0
  76. package/src/tools/pattern-schema.ts +51 -0
  77. package/src/tools/report-generator-tool.ts +134 -11
  78. package/src/tools/slither-tool.ts +61 -19
  79. package/src/tools/solodit-search-tool.ts +92 -14
  80. package/src/utils/audit-artifact-detector.ts +119 -0
  81. package/src/utils/dependency-scanner.ts +93 -0
  82. package/src/utils/project-detector.ts +128 -26
  83. package/src/utils/solidity-parser.ts +20 -4
  84. package/src/utils/solodit-health.ts +29 -0
@@ -0,0 +1,226 @@
1
+ import { existsSync, readdirSync, readFileSync, type Dirent } from "node:fs"
2
+ import { homedir } from "node:os"
3
+ import { basename, extname, join, resolve } from "node:path"
4
+ import type { ArgusConfig } from "../config/types"
5
+ import { createLogger } from "../shared/logger"
6
+ import { parseFrontmatter, validateSkillFrontmatter } from "./skill-schema"
7
+
8
+ export type ResolvedSkill = {
9
+ name: string
10
+ description: string
11
+ filePath: string
12
+ source: "bundled" | "custom" | "trailofbits" | "opencode" | "claude"
13
+ content: string
14
+ source_url?: string
15
+ source_license?: string
16
+ imported_at?: string
17
+ source_hash?: string
18
+ }
19
+
20
+ const OMO_PROJECT_SKILLS_DIR = [".opencode", "skills"]
21
+ const OMO_GLOBAL_SKILLS_DIR = [".config", "opencode", "skills"]
22
+ const CLAUDE_PROJECT_SKILLS_DIR = [".claude", "skills"]
23
+ const CLAUDE_GLOBAL_SKILLS_DIR = [".claude", "skills"]
24
+ const TOB_CACHE_DIR = join(homedir(), ".cache", "solidity-argus", "trailofbits-skills")
25
+
26
+ const SKILL_NAME_ALIASES: Record<string, string> = {
27
+ "vulnerability-patterns/reentrancy": "reentrancy",
28
+ "vulnerability-patterns/oracle-manipulation": "oracle-manipulation",
29
+ "vulnerability-patterns/access-control": "access-control",
30
+ "protocol-patterns/amm-dex": "amm-dex",
31
+ "protocol-patterns/lending-borrowing": "lending-borrowing",
32
+ "checklists/cyfrin-best-practices-upgrades": "cyfrin-best-practices-upgrades",
33
+ "references/exploit-reference": "exploit-reference",
34
+ "building-secure-contracts/token-integration-analyzer": "token-integration-analyzer",
35
+ }
36
+
37
+ function inferSkillNameFromPath(filePath: string): string {
38
+ if (basename(filePath) === "SKILL.md") {
39
+ return basename(resolve(filePath, ".."))
40
+ }
41
+ return basename(filePath, extname(filePath))
42
+ }
43
+
44
+ function parseSkillNameFromFrontmatter(content: string): string | null {
45
+ const match = content.match(/^name:\s*(.+)$/m)
46
+ if (!match) return null
47
+ return match[1]?.trim().replace(/^"|"$/g, "") ?? null
48
+ }
49
+
50
+ function parseSkillDescriptionFromFrontmatter(content: string): string {
51
+ const match = content.match(/^description:\s*(.+)$/m)
52
+ if (!match) return ""
53
+ const raw = match[1]?.trim() ?? ""
54
+ if (raw === ">" || raw === ">-") return ""
55
+ return raw.replace(/^"|"$/g, "")
56
+ }
57
+
58
+ function collectMarkdownFiles(root: string, maxDepth = 8): string[] {
59
+ if (!existsSync(root)) return []
60
+
61
+ const files: string[] = []
62
+ const stack: Array<{ dir: string; depth: number }> = [{ dir: root, depth: 0 }]
63
+
64
+ while (stack.length > 0) {
65
+ const current = stack.pop()
66
+ if (!current) continue
67
+ const { dir, depth } = current
68
+
69
+ let entries: Dirent[]
70
+ try {
71
+ entries = readdirSync(dir, { withFileTypes: true })
72
+ } catch {
73
+ continue
74
+ }
75
+
76
+ for (const entry of entries) {
77
+ const fullPath = join(dir, entry.name)
78
+ if (entry.isDirectory()) {
79
+ if (depth < maxDepth) stack.push({ dir: fullPath, depth: depth + 1 })
80
+ continue
81
+ }
82
+
83
+ if (!entry.isFile()) continue
84
+ if (extname(entry.name).toLowerCase() !== ".md") continue
85
+ files.push(fullPath)
86
+ }
87
+ }
88
+
89
+ return files
90
+ }
91
+
92
+ function getTrailOfBitsRoots(): string[] {
93
+ const pluginsDir = join(TOB_CACHE_DIR, "plugins")
94
+ if (!existsSync(pluginsDir)) return []
95
+
96
+ let entries: Dirent[]
97
+ try {
98
+ entries = readdirSync(pluginsDir, { withFileTypes: true })
99
+ } catch {
100
+ return []
101
+ }
102
+
103
+ const roots: string[] = []
104
+ for (const entry of entries) {
105
+ if (!entry.isDirectory()) continue
106
+ const skillsDir = join(pluginsDir, entry.name, "skills")
107
+ if (existsSync(skillsDir)) roots.push(skillsDir)
108
+ }
109
+ return roots
110
+ }
111
+
112
+ export function normalizeSkillName(input: string): string {
113
+ const trimmed = input.trim()
114
+ const alias = SKILL_NAME_ALIASES[trimmed]
115
+ if (alias) return alias
116
+ if (trimmed.includes("/")) {
117
+ const last = trimmed.split("/").at(-1)
118
+ if (last) return last
119
+ }
120
+ return trimmed
121
+ }
122
+
123
+ type SkillRoot = {
124
+ path: string
125
+ source: ResolvedSkill["source"]
126
+ }
127
+
128
+ function resolveCustomSkillsRoot(projectDir: string, argusConfig?: ArgusConfig): string | null {
129
+ const customSkillsDir = argusConfig?.knowledge?.customSkillsDir
130
+ if (!customSkillsDir) return null
131
+ const resolvedCustom = customSkillsDir.startsWith("/")
132
+ ? customSkillsDir
133
+ : resolve(projectDir, customSkillsDir)
134
+ return existsSync(resolvedCustom) ? resolvedCustom : null
135
+ }
136
+
137
+ export function resolveSkillRoots(projectDir: string, argusConfig?: ArgusConfig): SkillRoot[] {
138
+ const precedence = argusConfig?.knowledge?.skillPrecedence ?? "bundled-first"
139
+
140
+ const bundledRoot: SkillRoot = { path: resolve(import.meta.dir, "../../skills"), source: "bundled" }
141
+ const customRoot = resolveCustomSkillsRoot(projectDir, argusConfig)
142
+ const customSkillRoot: SkillRoot | null = customRoot ? { path: customRoot, source: "custom" } : null
143
+
144
+ const roots: SkillRoot[] = []
145
+
146
+ if (precedence === "custom-first") {
147
+ if (customSkillRoot) roots.push(customSkillRoot)
148
+ roots.push(bundledRoot)
149
+ } else {
150
+ roots.push(bundledRoot)
151
+ if (customSkillRoot) roots.push(customSkillRoot)
152
+ }
153
+
154
+ for (const tobRoot of getTrailOfBitsRoots()) {
155
+ roots.push({ path: tobRoot, source: "trailofbits" })
156
+ }
157
+
158
+ roots.push({ path: join(projectDir, ...OMO_PROJECT_SKILLS_DIR), source: "opencode" })
159
+ roots.push({ path: join(homedir(), ...OMO_GLOBAL_SKILLS_DIR), source: "opencode" })
160
+ roots.push({ path: join(projectDir, ...CLAUDE_PROJECT_SKILLS_DIR), source: "claude" })
161
+ roots.push({ path: join(homedir(), ...CLAUDE_GLOBAL_SKILLS_DIR), source: "claude" })
162
+
163
+ const seen = new Set<string>()
164
+ return roots.filter((root) => {
165
+ if (!existsSync(root.path)) return false
166
+ if (seen.has(root.path)) return false
167
+ seen.add(root.path)
168
+ return true
169
+ })
170
+ }
171
+
172
+ export function resolveArgusSkills(projectDir: string, argusConfig?: ArgusConfig): Map<string, ResolvedSkill> {
173
+ const resolved = new Map<string, ResolvedSkill>()
174
+ const roots = resolveSkillRoots(projectDir, argusConfig)
175
+ const logger = createLogger()
176
+
177
+ for (const root of roots) {
178
+ const markdownFiles = collectMarkdownFiles(root.path)
179
+ for (const markdownFile of markdownFiles) {
180
+ let content: string
181
+ try {
182
+ content = readFileSync(markdownFile, "utf8")
183
+ } catch {
184
+ continue
185
+ }
186
+
187
+ const frontmatter = parseFrontmatter(content)
188
+ if (frontmatter) {
189
+ const validation = validateSkillFrontmatter(frontmatter)
190
+ if (!validation.success) {
191
+ logger.warn(`Skipping skill with invalid frontmatter: ${markdownFile} — ${validation.errors.join(", ")}`)
192
+ continue
193
+ }
194
+ }
195
+
196
+ const parsedName = parseSkillNameFromFrontmatter(content)
197
+ const rawName = parsedName || inferSkillNameFromPath(markdownFile)
198
+ const normalizedName = normalizeSkillName(rawName)
199
+ if (!normalizedName) continue
200
+ if (resolved.has(normalizedName)) continue
201
+
202
+ const skill: ResolvedSkill = {
203
+ name: normalizedName,
204
+ description: parseSkillDescriptionFromFrontmatter(content),
205
+ filePath: markdownFile,
206
+ source: root.source,
207
+ content,
208
+ }
209
+
210
+ if (frontmatter) {
211
+ if (typeof frontmatter.source_url === "string") skill.source_url = frontmatter.source_url
212
+ if (typeof frontmatter.source_license === "string") skill.source_license = frontmatter.source_license
213
+ if (typeof frontmatter.imported_at === "string") skill.imported_at = frontmatter.imported_at
214
+ if (typeof frontmatter.source_hash === "string") skill.source_hash = frontmatter.source_hash
215
+ }
216
+
217
+ resolved.set(normalizedName, skill)
218
+ }
219
+ }
220
+
221
+ return resolved
222
+ }
223
+
224
+ export function getRequiredAuditSkills(): string[] {
225
+ return ["reentrancy", "oracle-manipulation", "amm-dex"]
226
+ }
@@ -0,0 +1,98 @@
1
+ import { z } from "zod"
2
+ import { parse as parseYaml } from "yaml"
3
+
4
+ export const DetectionRuleSchema = z.object({
5
+ regex: z.string(),
6
+ severity: z.enum(["Critical", "High", "Medium", "Low", "Informational"]),
7
+ confidence: z.enum(["High", "Medium", "Low"]).optional(),
8
+ swc: z.string().optional(),
9
+ description: z.string().optional(),
10
+ })
11
+
12
+ export const SkillFrontmatterSchema = z.object({
13
+ name: z
14
+ .string()
15
+ .min(1, "Skill name is required")
16
+ .max(128, "Skill name must be 128 characters or fewer")
17
+ .regex(/^[a-z0-9-]+$/, "Must be lowercase slug format (a-z, 0-9, hyphens only)"),
18
+ description: z.string().max(1024).default(""),
19
+ version: z
20
+ .string()
21
+ .regex(/^\d+\.\d+(\.\d+)?$/, "Must be semver format (e.g. 1.0.0)")
22
+ .optional(),
23
+ deprecated: z.boolean().optional(),
24
+ replacement: z.string().optional(),
25
+ category: z
26
+ .enum([
27
+ "vulnerability-pattern",
28
+ "methodology",
29
+ "protocol-pattern",
30
+ "checklist",
31
+ "reference",
32
+ ])
33
+ .optional(),
34
+ source_url: z.string().url().optional(),
35
+ source_license: z.string().optional(),
36
+ imported_at: z.string().optional(),
37
+ source_hash: z.string().optional(),
38
+ detection_rules: z.array(DetectionRuleSchema).optional(),
39
+ })
40
+
41
+ export type SkillFrontmatter = z.infer<typeof SkillFrontmatterSchema>
42
+
43
+ export function validateSkillFrontmatter(
44
+ frontmatter: Record<string, unknown>,
45
+ ): { success: true; data: SkillFrontmatter } | { success: false; errors: string[] } {
46
+ const result = SkillFrontmatterSchema.safeParse(frontmatter)
47
+ if (result.success) {
48
+ return { success: true, data: result.data }
49
+ }
50
+ return {
51
+ success: false,
52
+ errors: result.error.issues.map((issue) => {
53
+ const path = issue.path.length > 0 ? issue.path.join(".") : "root"
54
+ return `${path}: ${issue.message}`
55
+ }),
56
+ }
57
+ }
58
+
59
+ export function parseFrontmatter(content: string): Record<string, unknown> | null {
60
+ const fenceMatch = content.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---/)
61
+ if (!fenceMatch?.[1]) return null
62
+
63
+ const raw = fenceMatch[1]
64
+
65
+ if (raw.includes("detection_rules:")) {
66
+ try {
67
+ const parsed = parseYaml(raw)
68
+ if (typeof parsed === "object" && parsed !== null) {
69
+ return parsed as Record<string, unknown>
70
+ }
71
+ } catch {}
72
+ }
73
+
74
+ const lines = raw.split(/\r?\n/)
75
+ const result: Record<string, unknown> = {}
76
+
77
+ for (const line of lines) {
78
+ const kvMatch = line.match(/^([\w][\w-]*):\s*(.*)$/)
79
+ if (!kvMatch) continue
80
+
81
+ const key = kvMatch[1]!
82
+ let raw = kvMatch[2]?.trim() ?? ""
83
+
84
+ if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
85
+ raw = raw.slice(1, -1)
86
+ }
87
+
88
+ if (raw === "true") {
89
+ result[key] = true
90
+ } else if (raw === "false") {
91
+ result[key] = false
92
+ } else {
93
+ result[key] = raw
94
+ }
95
+ }
96
+
97
+ return Object.keys(result).length > 0 ? result : null
98
+ }
@@ -19,6 +19,8 @@ export function createAuditState(projectDir: string): {
19
19
  currentPhase: "reconnaissance",
20
20
  scope: [],
21
21
  startTime: Date.now(),
22
+ soloditResults: [],
23
+ fuzzCounterexamples: [],
22
24
  };
23
25
 
24
26
  const store = createFindingStore(state);
@@ -9,9 +9,35 @@ export interface Finding {
9
9
  description: string;
10
10
  file: string; // relative file path
11
11
  lines: [number, number]; // [start, end]
12
- source: "slither" | "manual" | "pattern" | "scvd";
12
+ source: "slither" | "manual" | "pattern" | "scvd" | "solodit" | "fuzz";
13
13
  remediation?: string;
14
14
  exploitReference?: string;
15
+ provenance?: {
16
+ timestamp: number;
17
+ toolVersion?: string;
18
+ phase?: AuditPhase;
19
+ };
20
+ }
21
+
22
+ export interface SoloditResult {
23
+ query: string;
24
+ timestamp: number;
25
+ resultCount: number;
26
+ topResults: Array<{
27
+ title: string;
28
+ severity: string;
29
+ url: string;
30
+ protocol: string;
31
+ }>;
32
+ }
33
+
34
+ export interface FuzzCounterexample {
35
+ testName: string;
36
+ inputs: string[];
37
+ revertReason?: string;
38
+ runs: number;
39
+ seed?: number;
40
+ timestamp: number;
15
41
  }
16
42
 
17
43
  export interface ContractProfile {
@@ -52,6 +78,11 @@ export interface AuditState {
52
78
  currentPhase: AuditPhase;
53
79
  scope: string[];
54
80
  startTime: number;
81
+ soloditResults?: SoloditResult[];
82
+ fuzzCounterexamples?: FuzzCounterexample[];
83
+ patternVersion?: string;
84
+ skillsLoaded?: string[];
85
+ unavailableTools?: string[];
55
86
  }
56
87
 
57
88
  export interface PersistentAuditState extends AuditState {
@@ -0,0 +1,73 @@
1
+ import { tool, type ToolContext } from "@opencode-ai/plugin"
2
+ import { loadArgusConfig } from "../config/loader"
3
+ import type { ArgusConfig } from "../config/types"
4
+ import { normalizeSkillName, resolveArgusSkills } from "../skills/argus-skill-resolver"
5
+
6
+ type ArgusSkillLoadArgs = {
7
+ name: string
8
+ }
9
+
10
+ type ArgusSkillLoadDependencies = {
11
+ loadConfig?: typeof loadArgusConfig
12
+ resolveSkills?: typeof resolveArgusSkills
13
+ }
14
+
15
+ export async function executeArgusSkillLoad(
16
+ args: ArgusSkillLoadArgs,
17
+ context: ToolContext,
18
+ deps: ArgusSkillLoadDependencies = {}
19
+ ): Promise<string> {
20
+ const projectDir = context.directory ?? context.worktree ?? process.cwd()
21
+ const loadConfig = deps.loadConfig ?? loadArgusConfig
22
+ const resolveSkills = deps.resolveSkills ?? resolveArgusSkills
23
+
24
+ let config: ArgusConfig | undefined
25
+ try {
26
+ config = loadConfig(projectDir)
27
+ } catch {
28
+ config = undefined
29
+ }
30
+
31
+ const normalizedName = normalizeSkillName(args.name)
32
+ const skills = resolveSkills(projectDir, config)
33
+ const skill = skills.get(normalizedName)
34
+
35
+ if (!skill) {
36
+ const available = Array.from(skills.keys()).sort().join(", ")
37
+ throw new Error(
38
+ `Argus skill "${args.name}" not found (normalized: "${normalizedName}"). Available Argus skills: ${available || "none"}`
39
+ )
40
+ }
41
+
42
+ const provenanceParts: string[] = []
43
+ if (skill.source_license) provenanceParts.push(skill.source_license)
44
+ if (skill.source_url) provenanceParts.push(skill.source_url)
45
+ if (skill.imported_at) provenanceParts.push(`Imported: ${skill.imported_at}`)
46
+
47
+ const provenanceLine = provenanceParts.length > 0 ? `[Provenance: ${provenanceParts.join(" | ")}]` : ""
48
+
49
+ return [
50
+ `## Argus Skill: ${skill.name} [Source: ${skill.source}]`,
51
+ "",
52
+ `**Source**: ${skill.source}`,
53
+ `**Path**: ${skill.filePath}`,
54
+ skill.description ? `**Description**: ${skill.description}` : "",
55
+ provenanceLine,
56
+ "",
57
+ skill.content,
58
+ ]
59
+ .filter(Boolean)
60
+ .join("\n")
61
+ }
62
+
63
+ export const argusSkillLoadTool = tool({
64
+ description: "Load Argus security skill content with OMO-compatible discovery and native fallback.",
65
+ args: {
66
+ name: tool.schema
67
+ .string()
68
+ .describe("Skill name (e.g., reentrancy, oracle-manipulation, or vulnerability-patterns/reentrancy)."),
69
+ },
70
+ async execute(args, context) {
71
+ return executeArgusSkillLoad(args, context)
72
+ },
73
+ })
@@ -1,6 +1,6 @@
1
1
  import os from "node:os";
2
2
  import { readdirSync, readFileSync, statSync } from "node:fs";
3
- import { extname, join, resolve } from "node:path";
3
+ import { dirname, extname, join, resolve } from "node:path";
4
4
  import { tool, type ToolContext } from "@opencode-ai/plugin";
5
5
  import {
6
6
  loadIndex,
@@ -8,6 +8,13 @@ import {
8
8
  type ScvdIndex,
9
9
  type ScvdIndexEntry,
10
10
  } from "../knowledge/scvd-index";
11
+ import {
12
+ extractDetectionRulesFromSkills,
13
+ loadPatternPacks,
14
+ } from "./pattern-loader";
15
+ import type { PatternDefinition } from "./pattern-schema";
16
+
17
+ export type PatternSource = "builtin" | "yaml" | "skill";
11
18
 
12
19
  export interface Match {
13
20
  pattern: string;
@@ -16,6 +23,8 @@ export interface Match {
16
23
  lines: [number, number];
17
24
  description: string;
18
25
  exploitReference?: string;
26
+ patternSource?: PatternSource;
27
+ category?: string;
19
28
  }
20
29
 
21
30
  export interface MatchSource {
@@ -28,6 +37,7 @@ export interface PatternCheckResult {
28
37
  patternsChecked: number;
29
38
  executionTime: number;
30
39
  target: string;
40
+ patternVersion?: string;
31
41
  }
32
42
 
33
43
  type PatternCheckArgs = {
@@ -51,8 +61,11 @@ type BuiltinPattern = {
51
61
  regex: RegExp;
52
62
  description: string;
53
63
  exploitReference?: string;
64
+ source?: PatternSource;
54
65
  };
55
66
 
67
+ export const PATTERN_PACK_VERSION = "1.0.0";
68
+
56
69
  const BUILTIN_PATTERNS: BuiltinPattern[] = [
57
70
  {
58
71
  name: "reentrancy",
@@ -113,6 +126,21 @@ function normalizeSeverity(value: string): Match["severity"] {
113
126
  return "Informational";
114
127
  }
115
128
 
129
+ function normalizePatternDefinitions(
130
+ patterns: PatternDefinition[],
131
+ source: PatternSource
132
+ ): BuiltinPattern[] {
133
+ return patterns.map((patternDef) => ({
134
+ name: patternDef.name,
135
+ category: patternDef.category,
136
+ severity: patternDef.severity,
137
+ regex: new RegExp(patternDef.regex),
138
+ description: patternDef.description,
139
+ ...(patternDef.exploit_ref ? { exploitReference: patternDef.exploit_ref } : {}),
140
+ source,
141
+ }));
142
+ }
143
+
116
144
  function uniqueScvdEntries(entries: ScvdIndexEntry[]): ScvdIndexEntry[] {
117
145
  const deduped = new Map<string, ScvdIndexEntry>();
118
146
  for (const entry of entries) {
@@ -127,7 +155,7 @@ async function collectScvdMatches(
127
155
  ): Promise<Match[]> {
128
156
  const detectedCategories = new Set<string>();
129
157
  for (const match of matches) {
130
- const category = PATTERN_NAME_TO_CATEGORY.get(match.pattern);
158
+ const category = match.category ?? PATTERN_NAME_TO_CATEGORY.get(match.pattern);
131
159
  if (category) {
132
160
  detectedCategories.add(category);
133
161
  }
@@ -171,7 +199,7 @@ async function collectScvdMatches(
171
199
  }));
172
200
  }
173
201
 
174
- function collectSolidityFiles(target: string): string[] {
202
+ function collectSolidityFiles(target: string, maxDepth = 8): string[] {
175
203
  const absoluteTarget = resolve(target);
176
204
  let stats: ReturnType<typeof statSync>;
177
205
 
@@ -190,19 +218,19 @@ function collectSolidityFiles(target: string): string[] {
190
218
  }
191
219
 
192
220
  const discovered: string[] = [];
193
- const stack = [absoluteTarget];
221
+ const stack: Array<{ path: string; depth: number }> = [{ path: absoluteTarget, depth: 0 }];
194
222
 
195
223
  while (stack.length > 0) {
196
224
  const current = stack.pop();
197
- if (!current) {
225
+ if (!current || current.depth > maxDepth) {
198
226
  continue;
199
227
  }
200
228
 
201
- const entries = readdirSync(current, { withFileTypes: true });
229
+ const entries = readdirSync(current.path, { withFileTypes: true });
202
230
  for (const entry of entries) {
203
- const fullPath = resolve(current, entry.name);
231
+ const fullPath = resolve(current.path, entry.name);
204
232
  if (entry.isDirectory()) {
205
- stack.push(fullPath);
233
+ stack.push({ path: fullPath, depth: current.depth + 1 });
206
234
  continue;
207
235
  }
208
236
 
@@ -252,6 +280,8 @@ function findMatches(file: string, patterns: BuiltinPattern[]): Match[] {
252
280
  lines: lineWindow(content, index),
253
281
  description: pattern.description,
254
282
  exploitReference: pattern.exploitReference,
283
+ patternSource: pattern.source ?? "builtin",
284
+ category: pattern.category,
255
285
  });
256
286
  }
257
287
  }
@@ -259,13 +289,16 @@ function findMatches(file: string, patterns: BuiltinPattern[]): Match[] {
259
289
  return matches;
260
290
  }
261
291
 
262
- function selectPatterns(categories?: string[]): BuiltinPattern[] {
292
+ function selectPatterns(
293
+ availablePatterns: BuiltinPattern[],
294
+ categories?: string[]
295
+ ): BuiltinPattern[] {
263
296
  if (!categories || categories.length === 0) {
264
- return BUILTIN_PATTERNS;
297
+ return availablePatterns;
265
298
  }
266
299
 
267
300
  const set = new Set(categories);
268
- return BUILTIN_PATTERNS.filter((pattern) => set.has(pattern.category));
301
+ return availablePatterns.filter((pattern) => set.has(pattern.category));
269
302
  }
270
303
 
271
304
  export async function executePatternCheck(
@@ -282,7 +315,17 @@ export async function executePatternCheck(
282
315
  const startedAt = Date.now();
283
316
  context.metadata({ title: `Pattern check: ${args.target}` });
284
317
 
285
- const selectedPatterns = selectPatterns(args.patterns);
318
+ const skillsDir = join(dirname(dirname(__dirname)), "skills");
319
+ const yamlPatterns = loadPatternPacks(join(skillsDir, "patterns"));
320
+ const skillDetectionRules = extractDetectionRulesFromSkills(skillsDir);
321
+
322
+ const allPatterns: BuiltinPattern[] = [
323
+ ...BUILTIN_PATTERNS.map((pattern) => ({ ...pattern, source: "builtin" as const })),
324
+ ...normalizePatternDefinitions(yamlPatterns, "yaml"),
325
+ ...normalizePatternDefinitions(skillDetectionRules, "skill"),
326
+ ];
327
+
328
+ const selectedPatterns = selectPatterns(allPatterns, args.patterns);
286
329
  const solidityFiles = collectSolidityFiles(args.target);
287
330
  if (solidityFiles.length === 0) {
288
331
  throw new Error(`No Solidity files found for target: ${args.target}`);
@@ -320,6 +363,7 @@ export async function executePatternCheck(
320
363
  patternsChecked: selectedPatterns.length,
321
364
  executionTime: Date.now() - startedAt,
322
365
  target: args.target,
366
+ patternVersion: PATTERN_PACK_VERSION,
323
367
  };
324
368
  }
325
369