solidity-argus 0.1.7 → 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.
- package/README.md +161 -1
- package/package.json +5 -2
- package/skills/README.md +63 -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 +11 -0
- package/skills/manifests/solodit.json +9 -0
- package/skills/manifests/sunweb3sec.json +11 -0
- package/skills/manifests/trailofbits.json +9 -0
- package/skills/methodology/audit-workflow/SKILL.md +3 -0
- package/skills/patterns/access-control.yaml +31 -0
- package/skills/patterns/erc4626.yaml +29 -0
- package/skills/patterns/flash-loan.yaml +20 -0
- package/skills/patterns/oracle.yaml +30 -0
- package/skills/patterns/proxy.yaml +30 -0
- package/skills/patterns/reentrancy.yaml +30 -0
- package/skills/patterns/signature.yaml +31 -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 +13 -0
- package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +6 -0
- package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +6 -0
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +13 -1
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +12 -0
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +13 -0
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +10 -1
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +13 -0
- package/skills/vulnerability-patterns/signature-malleability/SKILL.md +9 -0
- package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +11 -0
- package/src/agents/argus-prompt.ts +7 -7
- package/src/agents/pythia-prompt.ts +11 -11
- package/src/agents/scribe-prompt.ts +6 -6
- package/src/agents/sentinel-prompt.ts +7 -7
- package/src/cli/cli-output.ts +16 -0
- package/src/cli/cli-program.ts +9 -5
- package/src/cli/commands/doctor.ts +274 -16
- package/src/cli/commands/init.ts +5 -5
- package/src/cli/commands/install.ts +5 -5
- package/src/cli/commands/lint-skills.ts +114 -0
- package/src/cli/tui-prompts.ts +4 -2
- package/src/config/schema.ts +2 -0
- package/src/create-hooks.ts +141 -32
- package/src/create-tools.ts +2 -0
- package/src/features/error-recovery/session-recovery.ts +7 -1
- package/src/features/error-recovery/tool-error-recovery.ts +74 -19
- package/src/features/persistent-state/audit-state-manager.ts +36 -13
- package/src/hooks/agent-tracker.ts +53 -0
- package/src/hooks/compaction-hook.ts +46 -37
- package/src/hooks/config-handler.ts +22 -9
- package/src/hooks/context-budget.ts +45 -0
- package/src/hooks/event-hook-v2.ts +8 -2
- package/src/hooks/event-hook.ts +5 -4
- package/src/hooks/knowledge-sync-hook.ts +2 -1
- package/src/hooks/recon-context-builder.ts +66 -0
- package/src/hooks/safe-create-hook.ts +4 -5
- package/src/hooks/system-prompt-hook.ts +92 -221
- package/src/hooks/tool-tracking-hook.ts +108 -9
- package/src/hooks/types.ts +0 -1
- package/src/index.ts +28 -6
- package/src/knowledge/retry.ts +53 -0
- package/src/knowledge/scvd-client.ts +37 -10
- package/src/knowledge/scvd-errors.ts +89 -0
- package/src/knowledge/scvd-index.ts +53 -3
- package/src/knowledge/scvd-sync.ts +205 -34
- package/src/knowledge/source-manifest.ts +102 -0
- package/src/plugin-interface.ts +11 -3
- package/src/shared/binary-utils.ts +1 -0
- package/src/shared/logger.ts +78 -17
- package/src/skills/argus-skill-resolver.ts +226 -0
- package/src/skills/skill-schema.ts +98 -0
- package/src/state/audit-state.ts +2 -0
- package/src/state/types.ts +32 -1
- package/src/tools/argus-skill-load-tool.ts +73 -0
- package/src/tools/pattern-checker-tool.ts +56 -12
- package/src/tools/pattern-loader.ts +183 -0
- package/src/tools/pattern-schema.ts +51 -0
- package/src/tools/report-generator-tool.ts +134 -11
- package/src/tools/slither-tool.ts +61 -19
- package/src/tools/solodit-search-tool.ts +92 -14
- package/src/utils/audit-artifact-detector.ts +119 -0
- package/src/utils/dependency-scanner.ts +93 -0
- package/src/utils/project-detector.ts +128 -26
- package/src/utils/solidity-parser.ts +20 -4
- 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
|
+
}
|
package/src/state/audit-state.ts
CHANGED
package/src/state/types.ts
CHANGED
|
@@ -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(
|
|
292
|
+
function selectPatterns(
|
|
293
|
+
availablePatterns: BuiltinPattern[],
|
|
294
|
+
categories?: string[]
|
|
295
|
+
): BuiltinPattern[] {
|
|
263
296
|
if (!categories || categories.length === 0) {
|
|
264
|
-
return
|
|
297
|
+
return availablePatterns;
|
|
265
298
|
}
|
|
266
299
|
|
|
267
300
|
const set = new Set(categories);
|
|
268
|
-
return
|
|
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
|
|
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
|
|