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.
- 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 +4 -4
- package/src/agents/pythia-prompt.ts +4 -4
- package/src/agents/scribe-prompt.ts +3 -3
- package/src/agents/sentinel-prompt.ts +4 -4
- 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 +99 -14
- package/src/create-tools.ts +2 -0
- 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 +3 -0
- package/src/hooks/context-budget.ts +45 -0
- 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 +128 -0
- package/src/hooks/tool-tracking-hook.ts +86 -7
- package/src/index.ts +24 -1
- 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 +14 -1
- 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,183 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, existsSync } from "node:fs"
|
|
2
|
+
import { join, extname } from "node:path"
|
|
3
|
+
import { parse as parseYaml } from "yaml"
|
|
4
|
+
import { PatternPackSchema, type PatternDefinition } from "./pattern-schema"
|
|
5
|
+
import { createLogger } from "../shared/logger"
|
|
6
|
+
import { parseFrontmatter, SkillFrontmatterSchema } from "../skills/skill-schema"
|
|
7
|
+
|
|
8
|
+
const logger = createLogger()
|
|
9
|
+
|
|
10
|
+
const YAML_EXTENSIONS = new Set([".yaml", ".yml"])
|
|
11
|
+
|
|
12
|
+
const SKILL_NAME_TO_PATTERN_CATEGORY: Record<string, PatternDefinition["category"]> = {
|
|
13
|
+
"reentrancy": "reentrancy",
|
|
14
|
+
"access-control": "access-control",
|
|
15
|
+
"oracle-manipulation": "oracle-manipulation",
|
|
16
|
+
"flash-loan-attacks": "flash-loan",
|
|
17
|
+
"delegatecall-untrusted-callee": "proxy",
|
|
18
|
+
"authorization-txorigin": "access-control",
|
|
19
|
+
"unchecked-return-values": "logic-error",
|
|
20
|
+
"dos-revert": "dos",
|
|
21
|
+
"overflow-underflow": "logic-error",
|
|
22
|
+
"signature-malleability": "signature",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function loadPatternPacks(patternsDir: string): PatternDefinition[] {
|
|
26
|
+
if (!existsSync(patternsDir)) {
|
|
27
|
+
logger.warn(`Patterns directory does not exist: ${patternsDir}`)
|
|
28
|
+
return []
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const entries = readdirSync(patternsDir).filter((f) =>
|
|
32
|
+
YAML_EXTENSIONS.has(extname(f).toLowerCase())
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const allPatterns: PatternDefinition[] = []
|
|
36
|
+
|
|
37
|
+
for (const filename of entries) {
|
|
38
|
+
const filePath = join(patternsDir, filename)
|
|
39
|
+
try {
|
|
40
|
+
const raw = readFileSync(filePath, "utf-8")
|
|
41
|
+
const parsed = parseYaml(raw)
|
|
42
|
+
const result = PatternPackSchema.safeParse(parsed)
|
|
43
|
+
|
|
44
|
+
if (!result.success) {
|
|
45
|
+
logger.warn(
|
|
46
|
+
`Skipping ${filename}: schema validation failed — ${result.error.issues[0]?.message ?? "unknown"}`
|
|
47
|
+
)
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
allPatterns.push(...result.data.patterns)
|
|
52
|
+
} catch (err) {
|
|
53
|
+
logger.warn(
|
|
54
|
+
`Skipping ${filename}: ${err instanceof Error ? err.message : "parse error"}`
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return allPatterns
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function listSkillMarkdownFiles(skillsDir: string): string[] {
|
|
63
|
+
if (!existsSync(skillsDir)) {
|
|
64
|
+
logger.warn(`Skills directory does not exist: ${skillsDir}`)
|
|
65
|
+
return []
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const files: string[] = []
|
|
69
|
+
const stack = [skillsDir]
|
|
70
|
+
|
|
71
|
+
while (stack.length > 0) {
|
|
72
|
+
const current = stack.pop()
|
|
73
|
+
if (!current) continue
|
|
74
|
+
|
|
75
|
+
const entries = readdirSync(current, { withFileTypes: true })
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const fullPath = join(current, entry.name)
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
stack.push(fullPath)
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
84
|
+
files.push(fullPath)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return files
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function extractDetectionRulesFromSkills(skillsDir: string): PatternDefinition[] {
|
|
93
|
+
const skillFiles = listSkillMarkdownFiles(skillsDir)
|
|
94
|
+
const extracted: PatternDefinition[] = []
|
|
95
|
+
|
|
96
|
+
for (const filePath of skillFiles) {
|
|
97
|
+
try {
|
|
98
|
+
const content = readFileSync(filePath, "utf-8")
|
|
99
|
+
const frontmatter = parseFrontmatter(content)
|
|
100
|
+
if (!frontmatter) continue
|
|
101
|
+
|
|
102
|
+
const parsed = SkillFrontmatterSchema.safeParse(frontmatter)
|
|
103
|
+
if (!parsed.success) continue
|
|
104
|
+
|
|
105
|
+
const skillName = parsed.data.name
|
|
106
|
+
const category = SKILL_NAME_TO_PATTERN_CATEGORY[skillName]
|
|
107
|
+
if (!category) continue
|
|
108
|
+
|
|
109
|
+
const rules = parsed.data.detection_rules
|
|
110
|
+
if (!rules || rules.length === 0) continue
|
|
111
|
+
|
|
112
|
+
for (const [index, rule] of rules.entries()) {
|
|
113
|
+
extracted.push({
|
|
114
|
+
name: `${skillName}-rule-${index + 1}`,
|
|
115
|
+
category,
|
|
116
|
+
severity: rule.severity,
|
|
117
|
+
confidence: rule.confidence ?? "Medium",
|
|
118
|
+
version: "1.0",
|
|
119
|
+
regex: rule.regex,
|
|
120
|
+
description: rule.description ?? `Detection rule from ${skillName} SKILL.md`,
|
|
121
|
+
...(rule.swc ? { swc: rule.swc } : {}),
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
} catch (err) {
|
|
125
|
+
logger.warn(
|
|
126
|
+
`Skipping ${filePath}: ${err instanceof Error ? err.message : "parse error"}`
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return extracted
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
type BuiltinPattern = {
|
|
135
|
+
name: string
|
|
136
|
+
category: string
|
|
137
|
+
severity: string
|
|
138
|
+
regex: RegExp
|
|
139
|
+
description: string
|
|
140
|
+
exploitReference?: string
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isValidUrl(s: string): boolean {
|
|
144
|
+
try {
|
|
145
|
+
new URL(s)
|
|
146
|
+
return true
|
|
147
|
+
} catch {
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function builtinToDefinition(b: BuiltinPattern): PatternDefinition {
|
|
153
|
+
return {
|
|
154
|
+
name: b.name,
|
|
155
|
+
category: b.category as PatternDefinition["category"],
|
|
156
|
+
severity: b.severity as PatternDefinition["severity"],
|
|
157
|
+
confidence: "Medium",
|
|
158
|
+
version: "1.0",
|
|
159
|
+
regex: b.regex.source,
|
|
160
|
+
description: b.description,
|
|
161
|
+
...(b.exploitReference && isValidUrl(b.exploitReference)
|
|
162
|
+
? { exploit_ref: b.exploitReference }
|
|
163
|
+
: {}),
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function mergeWithBuiltins(
|
|
168
|
+
yamlPatterns: PatternDefinition[],
|
|
169
|
+
builtins: BuiltinPattern[],
|
|
170
|
+
skillDetectionRules: PatternDefinition[] = []
|
|
171
|
+
): PatternDefinition[] {
|
|
172
|
+
const mergedInputs = [...yamlPatterns, ...skillDetectionRules]
|
|
173
|
+
const yamlByName = new Map(mergedInputs.map((p) => [p.name, p]))
|
|
174
|
+
const merged: PatternDefinition[] = [...mergedInputs]
|
|
175
|
+
|
|
176
|
+
for (const builtin of builtins) {
|
|
177
|
+
if (!yamlByName.has(builtin.name)) {
|
|
178
|
+
merged.push(builtinToDefinition(builtin))
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return merged
|
|
183
|
+
}
|
|
@@ -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>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { tool, type ToolContext } from "@opencode-ai/plugin";
|
|
2
|
-
import type { AuditState, Finding, FindingSeverity } from "../state/types";
|
|
2
|
+
import type { AuditState, Finding, FindingSeverity, ToolExecution } from "../state/types";
|
|
3
3
|
|
|
4
4
|
type SeverityThreshold = "critical" | "high" | "medium" | "low" | "informational";
|
|
5
5
|
|
|
@@ -67,7 +67,20 @@ function emptyCounts(): FindingsCount {
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function
|
|
70
|
+
function emptyAuditState(findings: Finding[] = []): AuditState {
|
|
71
|
+
return {
|
|
72
|
+
sessionId: "",
|
|
73
|
+
projectDir: "",
|
|
74
|
+
contractsReviewed: [],
|
|
75
|
+
findings,
|
|
76
|
+
toolsExecuted: [],
|
|
77
|
+
currentPhase: "complete",
|
|
78
|
+
scope: [],
|
|
79
|
+
startTime: 0,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function parseAuditState(auditState: string): AuditState {
|
|
71
84
|
let parsed: unknown;
|
|
72
85
|
try {
|
|
73
86
|
parsed = JSON.parse(auditState);
|
|
@@ -76,14 +89,18 @@ function parseAuditState(auditState: string): Finding[] {
|
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
if (Array.isArray(parsed)) {
|
|
79
|
-
return parsed as Finding[];
|
|
92
|
+
return emptyAuditState(parsed as Finding[]);
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
if (typeof parsed === "object" && parsed !== null && Array.isArray((parsed as AuditState).findings)) {
|
|
83
|
-
|
|
96
|
+
const state = parsed as AuditState;
|
|
97
|
+
return {
|
|
98
|
+
...emptyAuditState(),
|
|
99
|
+
...state,
|
|
100
|
+
};
|
|
84
101
|
}
|
|
85
102
|
|
|
86
|
-
return
|
|
103
|
+
return emptyAuditState();
|
|
87
104
|
}
|
|
88
105
|
|
|
89
106
|
function normalizeTitle(check: string): string {
|
|
@@ -215,13 +232,123 @@ function buildFindingsSection(findings: Finding[]): string {
|
|
|
215
232
|
return lines.join("\n");
|
|
216
233
|
}
|
|
217
234
|
|
|
235
|
+
function formatDuration(ms: number): string {
|
|
236
|
+
if (ms < 1000) return `${ms}ms`;
|
|
237
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function buildProvenanceAppendix(
|
|
241
|
+
state: AuditState,
|
|
242
|
+
threshold: SeverityThreshold,
|
|
243
|
+
includedCount: number,
|
|
244
|
+
): string {
|
|
245
|
+
const lines: string[] = ["## Appendix: Data Provenance"];
|
|
246
|
+
|
|
247
|
+
lines.push("- Data source: `audit_state` payload");
|
|
248
|
+
lines.push(`- Severity threshold applied: ${threshold}`);
|
|
249
|
+
lines.push(`- Findings included in report: ${includedCount}`);
|
|
250
|
+
|
|
251
|
+
if (state.findings.length > 0) {
|
|
252
|
+
const sourceCounts: Record<string, number> = {};
|
|
253
|
+
for (const f of state.findings) {
|
|
254
|
+
sourceCounts[f.source] = (sourceCounts[f.source] ?? 0) + 1;
|
|
255
|
+
}
|
|
256
|
+
lines.push("");
|
|
257
|
+
lines.push("### Source Breakdown");
|
|
258
|
+
lines.push("");
|
|
259
|
+
lines.push("| Source | Count |");
|
|
260
|
+
lines.push("| --- | ---: |");
|
|
261
|
+
for (const [source, count] of Object.entries(sourceCounts).sort(
|
|
262
|
+
(a, b) => b[1] - a[1],
|
|
263
|
+
)) {
|
|
264
|
+
lines.push(`| ${source} | ${count} |`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (state.toolsExecuted.length > 0) {
|
|
269
|
+
lines.push("");
|
|
270
|
+
lines.push("### Tool Execution Summary");
|
|
271
|
+
lines.push("");
|
|
272
|
+
lines.push("| Tool | Duration | Status | Findings |");
|
|
273
|
+
lines.push("| --- | --- | --- | ---: |");
|
|
274
|
+
for (const exec of state.toolsExecuted) {
|
|
275
|
+
const duration =
|
|
276
|
+
exec.endTime != null
|
|
277
|
+
? formatDuration(exec.endTime - exec.startTime)
|
|
278
|
+
: "—";
|
|
279
|
+
const status = exec.success ? "✅ success" : "❌ failure";
|
|
280
|
+
lines.push(
|
|
281
|
+
`| ${exec.tool} | ${duration} | ${status} | ${exec.findingsCount} |`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const syncExec = state.toolsExecuted.find((t) => t.tool === "argus_sync_knowledge");
|
|
287
|
+
if (state.patternVersion || syncExec) {
|
|
288
|
+
lines.push("");
|
|
289
|
+
lines.push("### Data Freshness");
|
|
290
|
+
lines.push("");
|
|
291
|
+
if (state.patternVersion) {
|
|
292
|
+
lines.push(`- Pattern pack version: \`${state.patternVersion}\``);
|
|
293
|
+
}
|
|
294
|
+
if (syncExec) {
|
|
295
|
+
lines.push(`- SCVD last synced: ${new Date(syncExec.startTime).toISOString()}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (state.soloditResults && state.soloditResults.length > 0) {
|
|
300
|
+
lines.push("");
|
|
301
|
+
lines.push("### Solodit Cross-References");
|
|
302
|
+
lines.push("");
|
|
303
|
+
for (const result of state.soloditResults) {
|
|
304
|
+
lines.push(`**Query**: "${result.query}" — ${result.resultCount} results`);
|
|
305
|
+
if (result.topResults.length > 0) {
|
|
306
|
+
lines.push("");
|
|
307
|
+
lines.push("| Title | Severity | Protocol |");
|
|
308
|
+
lines.push("| --- | --- | --- |");
|
|
309
|
+
for (const top of result.topResults) {
|
|
310
|
+
lines.push(`| ${top.title} | ${top.severity} | ${top.protocol} |`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
lines.push("");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (state.fuzzCounterexamples && state.fuzzCounterexamples.length > 0) {
|
|
318
|
+
lines.push("");
|
|
319
|
+
lines.push("### Fuzz Evidence");
|
|
320
|
+
lines.push("");
|
|
321
|
+
lines.push("| Test | Inputs | Runs | Revert Reason |");
|
|
322
|
+
lines.push("| --- | --- | ---: | --- |");
|
|
323
|
+
for (const cx of state.fuzzCounterexamples) {
|
|
324
|
+
const inputs = cx.inputs.join(", ");
|
|
325
|
+
const reason = cx.revertReason ?? "—";
|
|
326
|
+
lines.push(`| ${cx.testName} | ${inputs} | ${cx.runs} | ${reason} |`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (state.skillsLoaded && state.skillsLoaded.length > 0) {
|
|
331
|
+
lines.push("");
|
|
332
|
+
lines.push("### Knowledge Sources");
|
|
333
|
+
lines.push("");
|
|
334
|
+
lines.push("Skills loaded during this audit:");
|
|
335
|
+
lines.push("");
|
|
336
|
+
for (const skill of state.skillsLoaded) {
|
|
337
|
+
lines.push(`- ${skill}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return lines.join("\n");
|
|
342
|
+
}
|
|
343
|
+
|
|
218
344
|
export async function executeReportGeneration(
|
|
219
345
|
args: ReportGeneratorArgs,
|
|
220
346
|
context: ToolContext
|
|
221
347
|
): Promise<ReportGenerationResult> {
|
|
222
348
|
const includeExecutiveSummary = args.include_executive_summary ?? true;
|
|
223
349
|
const threshold = args.severity_threshold ?? "low";
|
|
224
|
-
const
|
|
350
|
+
const state = parseAuditState(args.audit_state);
|
|
351
|
+
const findings = state.findings.filter((finding) =>
|
|
225
352
|
shouldIncludeFinding(finding, threshold)
|
|
226
353
|
);
|
|
227
354
|
const counts = calculateCounts(findings);
|
|
@@ -276,11 +403,7 @@ export async function executeReportGeneration(
|
|
|
276
403
|
sections.push(`- ${item}`);
|
|
277
404
|
}
|
|
278
405
|
|
|
279
|
-
sections.push(
|
|
280
|
-
sections.push("Tool execution summary:");
|
|
281
|
-
sections.push("- Data source: `audit_state` payload");
|
|
282
|
-
sections.push(`- Severity threshold applied: ${threshold}`);
|
|
283
|
-
sections.push(`- Findings included in report: ${findings.length}`);
|
|
406
|
+
sections.push(buildProvenanceAppendix(state, threshold, findings.length));
|
|
284
407
|
|
|
285
408
|
return {
|
|
286
409
|
report: sections.join("\n\n"),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "crypto";
|
|
2
2
|
import { mkdtempSync, writeFileSync, rmSync, readFileSync, existsSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { join, resolve, dirname, isAbsolute } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { execSync } from "node:child_process";
|
|
6
6
|
import { tool, type ToolContext } from "@opencode-ai/plugin";
|
|
@@ -44,7 +44,8 @@ export type SlitherRunResult = {
|
|
|
44
44
|
|
|
45
45
|
export type RunSlitherCommand = (
|
|
46
46
|
command: string[],
|
|
47
|
-
signal: AbortSignal
|
|
47
|
+
signal: AbortSignal,
|
|
48
|
+
cwd: string
|
|
48
49
|
) => Promise<SlitherRunResult>;
|
|
49
50
|
|
|
50
51
|
export type SlitherAnalyzeResult = {
|
|
@@ -167,8 +168,9 @@ function ensureSolc(version: string): boolean {
|
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
export const runSlitherCommand: RunSlitherCommand = async (command, signal) => {
|
|
171
|
+
export const runSlitherCommand: RunSlitherCommand = async (command, signal, cwd) => {
|
|
171
172
|
const child = Bun.spawn(command, {
|
|
173
|
+
cwd,
|
|
172
174
|
stdout: "pipe",
|
|
173
175
|
stderr: "pipe",
|
|
174
176
|
signal,
|
|
@@ -194,6 +196,7 @@ export type FlattenFallbackDeps = {
|
|
|
194
196
|
parseSolcVersion: (target: string) => string | undefined;
|
|
195
197
|
extractContractNames: (filePath: string) => string[];
|
|
196
198
|
execSyncFn: typeof execSync;
|
|
199
|
+
cwd: string;
|
|
197
200
|
};
|
|
198
201
|
|
|
199
202
|
const defaultFlattenDeps: FlattenFallbackDeps = {
|
|
@@ -203,6 +206,7 @@ const defaultFlattenDeps: FlattenFallbackDeps = {
|
|
|
203
206
|
parseSolcVersion,
|
|
204
207
|
extractContractNames,
|
|
205
208
|
execSyncFn: execSync,
|
|
209
|
+
cwd: process.cwd(),
|
|
206
210
|
};
|
|
207
211
|
|
|
208
212
|
export async function flattenFallback(
|
|
@@ -213,12 +217,26 @@ export async function flattenFallback(
|
|
|
213
217
|
const startedAt = Date.now();
|
|
214
218
|
|
|
215
219
|
if (!deps.hasBinary("forge")) {
|
|
216
|
-
return
|
|
220
|
+
return {
|
|
221
|
+
success: false,
|
|
222
|
+
findingsCount: 0,
|
|
223
|
+
findings: [],
|
|
224
|
+
executionTime: Date.now() - startedAt,
|
|
225
|
+
errors: ["forge binary not found — required for via_ir flatten fallback"],
|
|
226
|
+
error: "forge binary not found — required for via_ir flatten fallback",
|
|
227
|
+
};
|
|
217
228
|
}
|
|
218
229
|
|
|
219
230
|
const solcVersion = args.solc_version ?? deps.parseSolcVersion(args.target);
|
|
220
231
|
if (!solcVersion) {
|
|
221
|
-
return
|
|
232
|
+
return {
|
|
233
|
+
success: false,
|
|
234
|
+
findingsCount: 0,
|
|
235
|
+
findings: [],
|
|
236
|
+
executionTime: Date.now() - startedAt,
|
|
237
|
+
errors: ["Could not determine solc version from foundry.toml or pragma — required for flatten fallback"],
|
|
238
|
+
error: "Could not determine solc version from foundry.toml or pragma — required for flatten fallback",
|
|
239
|
+
};
|
|
222
240
|
}
|
|
223
241
|
|
|
224
242
|
if (!deps.ensureSolc(solcVersion)) {
|
|
@@ -241,6 +259,7 @@ export async function flattenFallback(
|
|
|
241
259
|
solFiles = deps.execSyncFn(`find "${srcDir}" -name "*.sol" -maxdepth 3 -not -path "*/mocks/*" -not -path "*/test/*"`, {
|
|
242
260
|
encoding: "utf-8",
|
|
243
261
|
timeout: 5_000,
|
|
262
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
244
263
|
})
|
|
245
264
|
.trim()
|
|
246
265
|
.split("\n")
|
|
@@ -268,7 +287,8 @@ export async function flattenFallback(
|
|
|
268
287
|
const flattened = deps.execSyncFn(`forge flatten "${solFile}"`, {
|
|
269
288
|
encoding: "utf-8",
|
|
270
289
|
timeout: 30_000,
|
|
271
|
-
cwd:
|
|
290
|
+
cwd: deps.cwd,
|
|
291
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
272
292
|
});
|
|
273
293
|
writeFileSync(flatFile, flattened);
|
|
274
294
|
} catch (_e) {
|
|
@@ -286,7 +306,7 @@ export async function flattenFallback(
|
|
|
286
306
|
];
|
|
287
307
|
|
|
288
308
|
try {
|
|
289
|
-
const runResult = await deps.runCommand(command, context.abort);
|
|
309
|
+
const runResult = await deps.runCommand(command, context.abort, deps.cwd);
|
|
290
310
|
|
|
291
311
|
let payload: SlitherPayload;
|
|
292
312
|
try {
|
|
@@ -358,8 +378,10 @@ function parseFindings(payload: SlitherPayload): Finding[] {
|
|
|
358
378
|
export async function executeSlitherAnalyze(
|
|
359
379
|
args: SlitherArgs,
|
|
360
380
|
context: ToolContext,
|
|
361
|
-
runCommand: RunSlitherCommand = runSlitherCommand
|
|
381
|
+
runCommand: RunSlitherCommand = runSlitherCommand,
|
|
382
|
+
cwd?: string
|
|
362
383
|
): Promise<SlitherAnalyzeResult> {
|
|
384
|
+
const projectDir = cwd ?? context.directory ?? context.worktree ?? process.cwd();
|
|
363
385
|
const startedAt = Date.now();
|
|
364
386
|
context.metadata({ title: `Slither analysis: ${args.target}` });
|
|
365
387
|
|
|
@@ -367,6 +389,7 @@ export async function executeSlitherAnalyze(
|
|
|
367
389
|
const fallbackResult = await flattenFallback(args, context, {
|
|
368
390
|
...defaultFlattenDeps,
|
|
369
391
|
runCommand,
|
|
392
|
+
cwd: projectDir,
|
|
370
393
|
});
|
|
371
394
|
if (fallbackResult) return fallbackResult;
|
|
372
395
|
return {
|
|
@@ -382,7 +405,7 @@ export async function executeSlitherAnalyze(
|
|
|
382
405
|
const command = buildCommand(args);
|
|
383
406
|
|
|
384
407
|
try {
|
|
385
|
-
const runResult = await runCommand(command, context.abort);
|
|
408
|
+
const runResult = await runCommand(command, context.abort, projectDir);
|
|
386
409
|
const errors: string[] = [];
|
|
387
410
|
|
|
388
411
|
if (runResult.exitCode !== 0) {
|
|
@@ -401,6 +424,7 @@ export async function executeSlitherAnalyze(
|
|
|
401
424
|
const fallbackResult = await flattenFallback(args, context, {
|
|
402
425
|
...defaultFlattenDeps,
|
|
403
426
|
runCommand,
|
|
427
|
+
cwd: projectDir,
|
|
404
428
|
});
|
|
405
429
|
if (fallbackResult) return fallbackResult;
|
|
406
430
|
}
|
|
@@ -425,6 +449,7 @@ export async function executeSlitherAnalyze(
|
|
|
425
449
|
const fallbackResult = await flattenFallback(args, context, {
|
|
426
450
|
...defaultFlattenDeps,
|
|
427
451
|
runCommand,
|
|
452
|
+
cwd: projectDir,
|
|
428
453
|
});
|
|
429
454
|
if (fallbackResult) return fallbackResult;
|
|
430
455
|
}
|
|
@@ -474,15 +499,24 @@ export async function executeSlitherAnalyze(
|
|
|
474
499
|
}
|
|
475
500
|
|
|
476
501
|
export function detectViaIr(target: string): boolean {
|
|
477
|
-
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
502
|
+
let dir = resolve(target.endsWith(".sol") ? dirname(target) : target);
|
|
503
|
+
const root = resolve("/");
|
|
504
|
+
|
|
505
|
+
while (true) {
|
|
506
|
+
const foundryTomlPath = join(dir, "foundry.toml");
|
|
507
|
+
if (existsSync(foundryTomlPath)) {
|
|
508
|
+
try {
|
|
509
|
+
const content = readFileSync(foundryTomlPath, "utf-8");
|
|
510
|
+
if (/^\s*via[_-]ir\s*=\s*true/m.test(content)) return true;
|
|
511
|
+
} catch {
|
|
512
|
+
// unreadable file — keep walking
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (dir === root) break;
|
|
516
|
+
dir = dirname(dir);
|
|
485
517
|
}
|
|
518
|
+
|
|
519
|
+
return false;
|
|
486
520
|
}
|
|
487
521
|
|
|
488
522
|
export const slitherTool = tool({
|
|
@@ -493,10 +527,18 @@ export const slitherTool = tool({
|
|
|
493
527
|
detectors: tool.schema.array(tool.schema.string()).optional(),
|
|
494
528
|
exclude: tool.schema.array(tool.schema.string()).optional(),
|
|
495
529
|
solc_version: tool.schema.string().optional(),
|
|
530
|
+
via_ir: tool.schema.boolean().optional(),
|
|
496
531
|
},
|
|
497
532
|
async execute(args, context) {
|
|
498
|
-
const
|
|
499
|
-
const
|
|
533
|
+
const projectDir = context.directory ?? context.worktree ?? process.cwd();
|
|
534
|
+
const resolvedTarget = isAbsolute(args.target) ? args.target : resolve(projectDir, args.target);
|
|
535
|
+
const viaIr = args.via_ir ?? detectViaIr(resolvedTarget);
|
|
536
|
+
const result = await executeSlitherAnalyze(
|
|
537
|
+
{ ...args, target: resolvedTarget, via_ir: viaIr },
|
|
538
|
+
context,
|
|
539
|
+
runSlitherCommand,
|
|
540
|
+
projectDir,
|
|
541
|
+
);
|
|
500
542
|
return JSON.stringify(result);
|
|
501
543
|
},
|
|
502
544
|
});
|