solidity-argus 0.1.5 → 0.1.7
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/AGENTS.md +3 -3
- package/package.json +1 -1
- package/src/agents/argus-prompt.ts +20 -0
- package/src/agents/pythia-prompt.ts +16 -4
- package/src/agents/scribe-prompt.ts +12 -0
- package/src/agents/sentinel-prompt.ts +13 -0
- package/src/create-hooks.ts +6 -2
- package/src/features/error-recovery/tool-error-recovery.ts +13 -0
- package/src/hooks/config-handler.ts +55 -22
- package/src/hooks/system-prompt-hook.ts +135 -4
- package/src/tools/slither-tool.ts +39 -2
- package/src/utils/project-detector.ts +12 -0
package/AGENTS.md
CHANGED
|
@@ -20,18 +20,18 @@ CLI: `argus doctor`, `argus init`, `argus install`.
|
|
|
20
20
|
**Role**: Static analysis and testing specialist
|
|
21
21
|
**Description**: Finds vulnerabilities through Slither static analysis, Foundry testing, fuzzing, and pattern matching. The tactical executor — runs tools, writes PoC tests, and verifies findings. Dispatched by Argus during Automated Scanning and Testing & Verification phases.
|
|
22
22
|
**Model**: anthropic/claude-sonnet-4-6
|
|
23
|
-
**Tools**: argus_slither_analyze, argus_forge_test, argus_forge_fuzz, argus_analyze_contract, argus_check_patterns
|
|
23
|
+
**Tools**: argus_slither_analyze, argus_forge_test, argus_forge_fuzz, argus_analyze_contract, argus_check_patterns, skill
|
|
24
24
|
|
|
25
25
|
## pythia
|
|
26
26
|
|
|
27
27
|
**Role**: Vulnerability researcher
|
|
28
28
|
**Description**: Consults Solodit, SCVD, and the knowledge base to find historical precedents and known attack vectors. Searches 7,769+ real-world audit findings and 55 curated vulnerability pattern files. Dispatched by Argus during Vulnerability Research phase.
|
|
29
29
|
**Model**: anthropic/claude-sonnet-4-6
|
|
30
|
-
**Tools**: argus_solodit_search, argus_check_patterns
|
|
30
|
+
**Tools**: argus_solodit_search, argus_check_patterns, skill
|
|
31
31
|
|
|
32
32
|
## scribe
|
|
33
33
|
|
|
34
34
|
**Role**: Audit report writer
|
|
35
35
|
**Description**: Transforms raw findings into professional markdown audit reports. Produces structured output with severity classifications (Critical/High/Medium/Low/Informational), impact assessments, proof-of-concept steps, and actionable recommendations. Dispatched by Argus only after all analysis is complete.
|
|
36
36
|
**Model**: anthropic/claude-sonnet-4-6
|
|
37
|
-
**Tools**: argus_generate_report
|
|
37
|
+
**Tools**: argus_generate_report, skill
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solidity-argus",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Solidity smart contract security auditing plugin for OpenCode — 4 specialized agents, 8 tools, and a curated vulnerability knowledge base",
|
|
5
5
|
"keywords": ["solidity", "security", "audit", "opencode", "plugin", "smart-contract", "ethereum", "defi", "slither", "foundry"],
|
|
6
6
|
"author": "Apegurus",
|
|
@@ -267,6 +267,26 @@ Your subagents have access to these specialized tools. Know when to delegate eac
|
|
|
267
267
|
- **Purpose**: Updates the local vulnerability database (SCVD).
|
|
268
268
|
- **Note**: Run if you suspect your knowledge base is stale or if the tool reports it's offline.
|
|
269
269
|
|
|
270
|
+
## SKILL SYSTEM
|
|
271
|
+
|
|
272
|
+
You have access to OpenCode Skills through the \`skill\` tool. Skills are specialized knowledge modules and must be used proactively when they improve audit accuracy.
|
|
273
|
+
|
|
274
|
+
- **Curated skill map (load these first)**:
|
|
275
|
+
- **Reconnaissance**: \`protocol-patterns/amm-dex\`, \`protocol-patterns/lending-borrowing\`, \`protocol-patterns/bridges-cross-chain\`
|
|
276
|
+
- **Manual Review**: \`vulnerability-patterns/reentrancy\`, \`vulnerability-patterns/oracle-manipulation\`, \`vulnerability-patterns/access-control\`
|
|
277
|
+
- **Verification**: \`checklists/cyfrin-defi-core\`, \`methodology/severity-classification\`, \`methodology/report-template\`
|
|
278
|
+
|
|
279
|
+
- **Deterministic trigger rules**:
|
|
280
|
+
- If the protocol uses AMM reserves or pool math, load \`protocol-patterns/amm-dex\` before Attack Surface Mapping.
|
|
281
|
+
- If price feeds or spot prices influence critical state changes, load \`vulnerability-patterns/oracle-manipulation\` before severity assessment.
|
|
282
|
+
- If proxy/upgrade patterns are present, load \`checklists/cyfrin-best-practices-upgrades\` before final recommendations.
|
|
283
|
+
|
|
284
|
+
- **Trail of Bits skills**:
|
|
285
|
+
- For pre-audit deep context modeling and attack-surface grounding: \`audit-context-building\`
|
|
286
|
+
- For bug family expansion: \`variant-analysis\`
|
|
287
|
+
- For invariant/fuzz strategy: \`property-based-testing\`
|
|
288
|
+
- For token integration risk: \`token-integration-analyzer\` (Trail of Bits building-secure-contracts plugin)
|
|
289
|
+
|
|
270
290
|
## KEY AUDIT PRINCIPLES
|
|
271
291
|
|
|
272
292
|
Adopt these principles to think like a top-tier auditor.
|
|
@@ -90,10 +90,22 @@ You have two primary tools. Master them.
|
|
|
90
90
|
OpenCode has a powerful **Skills** system that allows you to load specialized knowledge modules.
|
|
91
91
|
|
|
92
92
|
**How to use**:
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
- **
|
|
93
|
+
- Load a relevant skill before deep research when protocol context is non-trivial.
|
|
94
|
+
- Prioritize \`vulnerability-patterns/*\`, \`protocol-patterns/*\`, and \`references/*\` skills for exploit precedent mapping.
|
|
95
|
+
- Use the \`skill\` tool directly when available to load the exact skill you need.
|
|
96
|
+
- **Curated skill map**:
|
|
97
|
+
- \`vulnerability-patterns/reentrancy\`, \`vulnerability-patterns/oracle-manipulation\`, \`vulnerability-patterns/flash-loan-attacks\`
|
|
98
|
+
- \`protocol-patterns/lending-borrowing\`, \`protocol-patterns/amm-dex\`
|
|
99
|
+
- \`references/exploit-reference\`
|
|
100
|
+
- **Deterministic trigger rules**:
|
|
101
|
+
- If you investigate spot-price dependencies, load \`vulnerability-patterns/oracle-manipulation\` first.
|
|
102
|
+
- If capital-efficient attacks or same-block loops are plausible, load \`vulnerability-patterns/flash-loan-attacks\` first.
|
|
103
|
+
- If the protocol integrates arbitrary ERC20s, load ToB \`token-integration-analyzer\` (building-secure-contracts plugin) before recommendation drafting.
|
|
104
|
+
- **Examples**:
|
|
105
|
+
- "I am loading \`reentrancy\` to cross-reference known exploit patterns and missed edge cases."
|
|
106
|
+
- "I am loading \`lending-borrowing\` to map lending-specific oracle and liquidation failure modes."
|
|
107
|
+
- "I am loading \`audit-context-building\` (Trail of Bits) to build a line-by-line system model before vulnerability hypothesis generation."
|
|
108
|
+
- You are a generalist researcher. Use Skills to become a specialist on demand.
|
|
97
109
|
|
|
98
110
|
## OUTPUT FORMAT
|
|
99
111
|
|
|
@@ -60,6 +60,18 @@ Before generating the report, verify:
|
|
|
60
60
|
3. **False Positives**: Do not include findings that have been marked as false positives during the analysis phase.
|
|
61
61
|
4. **Clarity**: Is the "Description" easy to understand for a developer? Is the "Recommendation" safe to implement?
|
|
62
62
|
|
|
63
|
+
## SKILL SYSTEM
|
|
64
|
+
|
|
65
|
+
Use the \`skill\` tool when needed to improve report quality and consistency.
|
|
66
|
+
|
|
67
|
+
- **Curated skill map**:
|
|
68
|
+
- \`methodology/report-template\`, \`methodology/severity-classification\`
|
|
69
|
+
- \`checklists/cyfrin-defi-core\`
|
|
70
|
+
- \`references/exploit-reference\`
|
|
71
|
+
- **Deterministic trigger rules**:
|
|
72
|
+
- If severity wording drifts, load \`methodology/severity-classification\` before publishing.
|
|
73
|
+
- If recommendation quality is generic, load \`checklists/cyfrin-defi-core\` before final edits.
|
|
74
|
+
|
|
63
75
|
## OUTPUT FORMAT
|
|
64
76
|
|
|
65
77
|
Write the full report in Markdown. Use the standard finding format:
|
|
@@ -87,6 +87,19 @@ You have access to a specific set of tools. Use them effectively.
|
|
|
87
87
|
**Interpretation**:
|
|
88
88
|
- Look at the \`counterexamples\`. They tell you exactly what inputs broke the code.
|
|
89
89
|
|
|
90
|
+
## SKILL SYSTEM
|
|
91
|
+
|
|
92
|
+
Use the \`skill\` tool to load specialized skills before deep verification work.
|
|
93
|
+
|
|
94
|
+
- **Curated skill map**:
|
|
95
|
+
- \`vulnerability-patterns/reentrancy\`, \`vulnerability-patterns/access-control\`, \`vulnerability-patterns/oracle-manipulation\`
|
|
96
|
+
- \`checklists/cyfrin-defi-integrations\`, \`methodology/severity-classification\`
|
|
97
|
+
- Trail of Bits: \`property-based-testing\`, \`variant-analysis\`
|
|
98
|
+
- **Deterministic trigger rules**:
|
|
99
|
+
- If external calls and mutable state interleave, load \`vulnerability-patterns/reentrancy\` before writing PoCs.
|
|
100
|
+
- If privileged flows are central to the finding, load \`vulnerability-patterns/access-control\` before severity scoring.
|
|
101
|
+
- If fuzzing strategy is unclear, load ToB \`property-based-testing\` before selecting invariants.
|
|
102
|
+
|
|
90
103
|
## OUTPUT FORMAT
|
|
91
104
|
|
|
92
105
|
Return your findings to Argus in this structured Markdown format. Do not deviate.
|
package/src/create-hooks.ts
CHANGED
|
@@ -33,7 +33,11 @@ export function createHooks(args: {
|
|
|
33
33
|
|
|
34
34
|
const systemPromptHook = isHookEnabled("system-prompt")
|
|
35
35
|
? safeCreateHook(
|
|
36
|
-
() =>
|
|
36
|
+
() =>
|
|
37
|
+
createSystemPromptHook(getAuditState, {
|
|
38
|
+
argusConfig: config,
|
|
39
|
+
projectDir,
|
|
40
|
+
}),
|
|
37
41
|
"system-prompt"
|
|
38
42
|
)
|
|
39
43
|
: undefined
|
|
@@ -54,7 +58,7 @@ export function createHooks(args: {
|
|
|
54
58
|
: undefined
|
|
55
59
|
|
|
56
60
|
return {
|
|
57
|
-
config: createConfigHandler(config),
|
|
61
|
+
config: createConfigHandler(config, projectDir),
|
|
58
62
|
"experimental.chat.system.transform": systemPromptHook
|
|
59
63
|
? async (_input, output) => {
|
|
60
64
|
const block = await systemPromptHook({
|
|
@@ -7,6 +7,8 @@ const RECOVERY_HINTS: Record<string, string> = {
|
|
|
7
7
|
scvd: "Check SCVD API at https://api.scvd.dev — may be temporarily unavailable",
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
const VIA_IR_HINT = "Project uses via_ir — Slither uses forge-flatten fallback automatically. Ensure forge and solc-select are installed."
|
|
11
|
+
|
|
10
12
|
export function createToolErrorRecoveryHandler() {
|
|
11
13
|
const logger = createLogger()
|
|
12
14
|
|
|
@@ -14,6 +16,17 @@ export function createToolErrorRecoveryHandler() {
|
|
|
14
16
|
const { tool, result } = toolResult
|
|
15
17
|
const lowerResult = result.toLowerCase()
|
|
16
18
|
|
|
19
|
+
const isViaIr =
|
|
20
|
+
lowerResult.includes("via_ir") ||
|
|
21
|
+
lowerResult.includes("via-ir") ||
|
|
22
|
+
lowerResult.includes("flatten fallback") ||
|
|
23
|
+
lowerResult.includes("flatten-fallback")
|
|
24
|
+
|
|
25
|
+
if (isViaIr && tool.includes("slither")) {
|
|
26
|
+
logger.info(`Tool error recovery hint for ${tool}: ${VIA_IR_HINT}`)
|
|
27
|
+
return `\n[Argus Recovery Hint] ${VIA_IR_HINT}`
|
|
28
|
+
}
|
|
29
|
+
|
|
17
30
|
const isError =
|
|
18
31
|
lowerResult.includes("enoent") ||
|
|
19
32
|
lowerResult.includes("not found") ||
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolve, join } from "node:path"
|
|
2
|
-
import { existsSync } from "node:fs"
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs"
|
|
3
3
|
import { homedir } from "node:os"
|
|
4
4
|
import { execSync } from "node:child_process"
|
|
5
5
|
import type { Config } from "@opencode-ai/sdk/v2"
|
|
@@ -14,21 +14,40 @@ import { SCRIBE_PROMPT } from "../agents/scribe-prompt"
|
|
|
14
14
|
const TOB_CACHE_DIR = join(homedir(), ".cache", "solidity-argus", "trailofbits-skills")
|
|
15
15
|
const TOB_REPO_URL = "https://github.com/trailofbits/skills.git"
|
|
16
16
|
|
|
17
|
-
function
|
|
18
|
-
|
|
17
|
+
function getTrailOfBitsSkillsPaths(rootDir: string): string[] {
|
|
18
|
+
const pluginsDir = join(rootDir, "plugins")
|
|
19
|
+
if (!existsSync(pluginsDir)) return []
|
|
20
|
+
|
|
21
|
+
const pluginEntries = readdirSync(pluginsDir, { withFileTypes: true })
|
|
22
|
+
const skillDirs: string[] = []
|
|
23
|
+
|
|
24
|
+
for (const entry of pluginEntries) {
|
|
25
|
+
if (!entry.isDirectory()) continue
|
|
26
|
+
const pluginSkillsDir = join(pluginsDir, entry.name, "skills")
|
|
27
|
+
if (existsSync(pluginSkillsDir)) {
|
|
28
|
+
skillDirs.push(pluginSkillsDir)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return skillDirs
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ensureTrailOfBitsSkills(): string[] {
|
|
36
|
+
if (existsSync(TOB_CACHE_DIR)) return getTrailOfBitsSkillsPaths(TOB_CACHE_DIR)
|
|
19
37
|
try {
|
|
20
38
|
execSync(`git clone --depth 1 ${TOB_REPO_URL} "${TOB_CACHE_DIR}"`, {
|
|
21
39
|
stdio: "ignore",
|
|
22
40
|
timeout: 30_000,
|
|
23
41
|
})
|
|
24
|
-
return TOB_CACHE_DIR
|
|
42
|
+
return getTrailOfBitsSkillsPaths(TOB_CACHE_DIR)
|
|
25
43
|
} catch (_e) {
|
|
26
|
-
return
|
|
44
|
+
return []
|
|
27
45
|
}
|
|
28
46
|
}
|
|
29
47
|
|
|
30
48
|
export function createConfigHandler(
|
|
31
|
-
argusConfig: ArgusConfig
|
|
49
|
+
argusConfig: ArgusConfig,
|
|
50
|
+
projectDir: string = process.cwd()
|
|
32
51
|
): (config: Config) => Promise<void> {
|
|
33
52
|
const triggerKnowledgeSync = createKnowledgeSyncHook(argusConfig)
|
|
34
53
|
|
|
@@ -50,6 +69,7 @@ export function createConfigHandler(
|
|
|
50
69
|
pythia: "allow",
|
|
51
70
|
scribe: "allow",
|
|
52
71
|
},
|
|
72
|
+
skill: "allow",
|
|
53
73
|
},
|
|
54
74
|
},
|
|
55
75
|
sentinel: {
|
|
@@ -57,32 +77,35 @@ export function createConfigHandler(
|
|
|
57
77
|
model: argusConfig.agents?.sentinel?.model ?? DEFAULT_MODELS.sentinel,
|
|
58
78
|
description: "Static analysis and testing specialist",
|
|
59
79
|
prompt: SENTINEL_PROMPT,
|
|
60
|
-
|
|
61
|
-
argus_slither_analyze:
|
|
62
|
-
argus_forge_test:
|
|
63
|
-
argus_forge_fuzz:
|
|
64
|
-
argus_analyze_contract:
|
|
65
|
-
argus_check_patterns:
|
|
66
|
-
|
|
80
|
+
permission: {
|
|
81
|
+
argus_slither_analyze: "allow",
|
|
82
|
+
argus_forge_test: "allow",
|
|
83
|
+
argus_forge_fuzz: "allow",
|
|
84
|
+
argus_analyze_contract: "allow",
|
|
85
|
+
argus_check_patterns: "allow",
|
|
86
|
+
skill: "allow",
|
|
87
|
+
},
|
|
67
88
|
},
|
|
68
89
|
pythia: {
|
|
69
90
|
mode: "subagent",
|
|
70
91
|
model: argusConfig.agents?.pythia?.model ?? DEFAULT_MODELS.pythia,
|
|
71
92
|
description: "Vulnerability researcher",
|
|
72
93
|
prompt: PYTHIA_PROMPT,
|
|
73
|
-
|
|
74
|
-
argus_solodit_search:
|
|
75
|
-
argus_check_patterns:
|
|
76
|
-
|
|
94
|
+
permission: {
|
|
95
|
+
argus_solodit_search: "allow",
|
|
96
|
+
argus_check_patterns: "allow",
|
|
97
|
+
skill: "allow",
|
|
98
|
+
},
|
|
77
99
|
},
|
|
78
100
|
scribe: {
|
|
79
101
|
mode: "subagent",
|
|
80
102
|
model: argusConfig.agents?.scribe?.model ?? DEFAULT_MODELS.scribe,
|
|
81
103
|
description: "Audit report writer",
|
|
82
104
|
prompt: SCRIBE_PROMPT,
|
|
83
|
-
|
|
84
|
-
argus_generate_report:
|
|
85
|
-
|
|
105
|
+
permission: {
|
|
106
|
+
argus_generate_report: "allow",
|
|
107
|
+
skill: "allow",
|
|
108
|
+
},
|
|
86
109
|
},
|
|
87
110
|
}
|
|
88
111
|
|
|
@@ -101,8 +124,18 @@ export function createConfigHandler(
|
|
|
101
124
|
const skillsPaths = [...(config.skills?.paths ?? [])]
|
|
102
125
|
skillsPaths.push(resolve(import.meta.dir, "../../skills"))
|
|
103
126
|
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
127
|
+
const customSkillsDir = argusConfig.knowledge?.customSkillsDir
|
|
128
|
+
if (customSkillsDir) {
|
|
129
|
+
const resolvedCustomSkillsDir = customSkillsDir.startsWith("/")
|
|
130
|
+
? customSkillsDir
|
|
131
|
+
: resolve(projectDir, customSkillsDir)
|
|
132
|
+
if (existsSync(resolvedCustomSkillsDir)) {
|
|
133
|
+
skillsPaths.push(resolvedCustomSkillsDir)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const tobSkillDirs = ensureTrailOfBitsSkills()
|
|
138
|
+
if (tobSkillDirs.length > 0) skillsPaths.push(...tobSkillDirs)
|
|
106
139
|
|
|
107
140
|
config.skills = {
|
|
108
141
|
...(config.skills ?? {}),
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import type { ArgusConfig } from "../config/types";
|
|
2
5
|
import type { AuditState, FindingSeverity } from "../state/types";
|
|
3
6
|
|
|
4
7
|
interface SystemPromptInput {
|
|
@@ -6,6 +9,19 @@ interface SystemPromptInput {
|
|
|
6
9
|
cwd: string;
|
|
7
10
|
}
|
|
8
11
|
|
|
12
|
+
interface SkillIndexEntry {
|
|
13
|
+
count: number;
|
|
14
|
+
sample: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SkillIndexSnapshot {
|
|
18
|
+
bundled: SkillIndexEntry;
|
|
19
|
+
trailOfBits: SkillIndexEntry;
|
|
20
|
+
custom: SkillIndexEntry;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const TOB_CACHE_DIR = join(homedir(), ".cache", "solidity-argus", "trailofbits-skills");
|
|
24
|
+
|
|
9
25
|
/**
|
|
10
26
|
* Checks if the given directory contains a Solidity project
|
|
11
27
|
* by looking for foundry.toml or hardhat.config.{js,ts}
|
|
@@ -66,11 +82,106 @@ function buildAuditStateSummary(state: AuditState | null): string {
|
|
|
66
82
|
].join("\n");
|
|
67
83
|
}
|
|
68
84
|
|
|
85
|
+
function collectSkillNamesFromRoot(rootPath: string): string[] {
|
|
86
|
+
if (!existsSync(rootPath)) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const rootEntries = readdirSync(rootPath, { withFileTypes: true });
|
|
91
|
+
const names = new Set<string>();
|
|
92
|
+
|
|
93
|
+
for (const rootEntry of rootEntries) {
|
|
94
|
+
if (!rootEntry.isDirectory()) continue;
|
|
95
|
+
|
|
96
|
+
const directSkill = join(rootPath, rootEntry.name, "SKILL.md");
|
|
97
|
+
if (existsSync(directSkill)) {
|
|
98
|
+
names.add(rootEntry.name);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const categoryDir = join(rootPath, rootEntry.name);
|
|
103
|
+
const categoryEntries = readdirSync(categoryDir, { withFileTypes: true });
|
|
104
|
+
|
|
105
|
+
for (const categoryEntry of categoryEntries) {
|
|
106
|
+
if (!categoryEntry.isDirectory()) continue;
|
|
107
|
+
const categorySkill = join(categoryDir, categoryEntry.name, "SKILL.md");
|
|
108
|
+
if (existsSync(categorySkill)) {
|
|
109
|
+
names.add(`${rootEntry.name}/${categoryEntry.name}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return Array.from(names).sort();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getTrailOfBitsSkillRoots(): string[] {
|
|
118
|
+
const pluginsDir = join(TOB_CACHE_DIR, "plugins");
|
|
119
|
+
if (!existsSync(pluginsDir)) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const pluginEntries = readdirSync(pluginsDir, { withFileTypes: true });
|
|
124
|
+
const roots: string[] = [];
|
|
125
|
+
|
|
126
|
+
for (const pluginEntry of pluginEntries) {
|
|
127
|
+
if (!pluginEntry.isDirectory()) continue;
|
|
128
|
+
const pluginSkillsRoot = join(pluginsDir, pluginEntry.name, "skills");
|
|
129
|
+
if (existsSync(pluginSkillsRoot)) {
|
|
130
|
+
roots.push(pluginSkillsRoot);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return roots;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function toSkillIndexEntry(skillNames: string[]): SkillIndexEntry {
|
|
138
|
+
return {
|
|
139
|
+
count: skillNames.length,
|
|
140
|
+
sample: skillNames.slice(0, 3),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function buildSkillIndexSnapshot(args: {
|
|
145
|
+
argusConfig?: ArgusConfig;
|
|
146
|
+
projectDir?: string;
|
|
147
|
+
}): SkillIndexSnapshot {
|
|
148
|
+
const bundledRoot = resolve(import.meta.dir, "../../skills");
|
|
149
|
+
const bundledSkills = collectSkillNamesFromRoot(bundledRoot);
|
|
150
|
+
|
|
151
|
+
const trailOfBitsSkills = new Set<string>();
|
|
152
|
+
for (const tobRoot of getTrailOfBitsSkillRoots()) {
|
|
153
|
+
for (const name of collectSkillNamesFromRoot(tobRoot)) {
|
|
154
|
+
trailOfBitsSkills.add(name);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const customDir = args.argusConfig?.knowledge?.customSkillsDir;
|
|
159
|
+
const customRoot = customDir
|
|
160
|
+
? customDir.startsWith("/")
|
|
161
|
+
? customDir
|
|
162
|
+
: resolve(args.projectDir ?? process.cwd(), customDir)
|
|
163
|
+
: undefined;
|
|
164
|
+
const customSkills = customRoot ? collectSkillNamesFromRoot(customRoot) : [];
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
bundled: toSkillIndexEntry(bundledSkills),
|
|
168
|
+
trailOfBits: toSkillIndexEntry(Array.from(trailOfBitsSkills).sort()),
|
|
169
|
+
custom: toSkillIndexEntry(customSkills),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function formatSkillSample(entry: SkillIndexEntry): string {
|
|
174
|
+
return entry.sample.length > 0 ? entry.sample.join(", ") : "none";
|
|
175
|
+
}
|
|
176
|
+
|
|
69
177
|
/**
|
|
70
178
|
* Builds the full audit context block to inject into the system prompt.
|
|
71
179
|
* Designed to be concise (500-800 tokens).
|
|
72
180
|
*/
|
|
73
|
-
function buildAuditContextBlock(
|
|
181
|
+
function buildAuditContextBlock(
|
|
182
|
+
state: AuditState | null,
|
|
183
|
+
skillIndex: SkillIndexSnapshot
|
|
184
|
+
): string {
|
|
74
185
|
return `
|
|
75
186
|
<argus-context>
|
|
76
187
|
## Solidity Audit Context
|
|
@@ -92,6 +203,20 @@ function buildAuditContextBlock(state: AuditState | null): string {
|
|
|
92
203
|
- \`argus_generate_report\`: Compile findings into structured audit report
|
|
93
204
|
- \`argus_sync_knowledge\`: Update local vulnerability database (SCVD)
|
|
94
205
|
|
|
206
|
+
### Available Skills
|
|
207
|
+
- \`vulnerability-patterns/*\`: 35+ exploit classes (reentrancy, oracle manipulation, flash loans)
|
|
208
|
+
- \`protocol-patterns/*\`: AMM/DEX, lending, bridges, governance, staking-specific heuristics
|
|
209
|
+
- \`methodology/*\`: audit workflow, severity calibration, and report structure guidance
|
|
210
|
+
- \`checklists/*\`: structured review checklists for upgrades, integrations, and DeFi best practices
|
|
211
|
+
- \`references/*\`: exploit references and vulnerable examples for historical precedent
|
|
212
|
+
- Trail of Bits skills include both audit-relevant and general engineering skills; prioritize security-audit-oriented skills
|
|
213
|
+
|
|
214
|
+
### Skill Index Snapshot
|
|
215
|
+
- Bundled skills: ${skillIndex.bundled.count} (examples: ${formatSkillSample(skillIndex.bundled)})
|
|
216
|
+
- Trail of Bits skills: ${skillIndex.trailOfBits.count} (examples: ${formatSkillSample(skillIndex.trailOfBits)})
|
|
217
|
+
- Custom project skills: ${skillIndex.custom.count} (examples: ${formatSkillSample(skillIndex.custom)})
|
|
218
|
+
- Use the \`skill\` tool with exact skill names from the catalog
|
|
219
|
+
|
|
95
220
|
### Audit State
|
|
96
221
|
${buildAuditStateSummary(state)}
|
|
97
222
|
|
|
@@ -109,8 +234,14 @@ Severity must follow classification above. Do not inflate severity.
|
|
|
109
234
|
* @returns Async transform function compatible with OpenCode's experimental.chat.system.transform
|
|
110
235
|
*/
|
|
111
236
|
export function createSystemPromptHook(
|
|
112
|
-
getAuditState: () => AuditState | null
|
|
237
|
+
getAuditState: () => AuditState | null,
|
|
238
|
+
options?: { argusConfig?: ArgusConfig; projectDir?: string }
|
|
113
239
|
): (input: SystemPromptInput) => Promise<string | null> {
|
|
240
|
+
const skillIndex = buildSkillIndexSnapshot({
|
|
241
|
+
argusConfig: options?.argusConfig,
|
|
242
|
+
projectDir: options?.projectDir,
|
|
243
|
+
});
|
|
244
|
+
|
|
114
245
|
return async (input: SystemPromptInput): Promise<string | null> => {
|
|
115
246
|
const isSolidity = await isSolidityProject(input.cwd);
|
|
116
247
|
|
|
@@ -119,7 +250,7 @@ export function createSystemPromptHook(
|
|
|
119
250
|
}
|
|
120
251
|
|
|
121
252
|
const auditState = getAuditState();
|
|
122
|
-
return buildAuditContextBlock(auditState);
|
|
253
|
+
return buildAuditContextBlock(auditState, skillIndex);
|
|
123
254
|
};
|
|
124
255
|
}
|
|
125
256
|
|
|
@@ -12,6 +12,7 @@ type SlitherArgs = {
|
|
|
12
12
|
detectors?: string[];
|
|
13
13
|
exclude?: string[];
|
|
14
14
|
solc_version?: string;
|
|
15
|
+
via_ir?: boolean;
|
|
15
16
|
};
|
|
16
17
|
|
|
17
18
|
type SlitherDetector = {
|
|
@@ -135,6 +136,12 @@ const FALLBACK_TRIGGERS = [
|
|
|
135
136
|
"crytic_compile",
|
|
136
137
|
"empty AST",
|
|
137
138
|
"Compilation failed",
|
|
139
|
+
"via_ir",
|
|
140
|
+
"via-ir",
|
|
141
|
+
"viaIR",
|
|
142
|
+
"YulException",
|
|
143
|
+
"StackTooDeep",
|
|
144
|
+
"Stack too deep",
|
|
138
145
|
];
|
|
139
146
|
|
|
140
147
|
function shouldTryFlattenFallback(errors: string[], stderr: string): boolean {
|
|
@@ -354,9 +361,26 @@ export async function executeSlitherAnalyze(
|
|
|
354
361
|
runCommand: RunSlitherCommand = runSlitherCommand
|
|
355
362
|
): Promise<SlitherAnalyzeResult> {
|
|
356
363
|
const startedAt = Date.now();
|
|
357
|
-
const command = buildCommand(args);
|
|
358
364
|
context.metadata({ title: `Slither analysis: ${args.target}` });
|
|
359
365
|
|
|
366
|
+
if (args.via_ir) {
|
|
367
|
+
const fallbackResult = await flattenFallback(args, context, {
|
|
368
|
+
...defaultFlattenDeps,
|
|
369
|
+
runCommand,
|
|
370
|
+
});
|
|
371
|
+
if (fallbackResult) return fallbackResult;
|
|
372
|
+
return {
|
|
373
|
+
success: false,
|
|
374
|
+
findingsCount: 0,
|
|
375
|
+
findings: [],
|
|
376
|
+
executionTime: Date.now() - startedAt,
|
|
377
|
+
errors: ["via_ir enabled — flatten fallback failed. Ensure forge and solc are available."],
|
|
378
|
+
error: "Project uses via_ir which is incompatible with Slither direct analysis. Flatten fallback also failed.",
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const command = buildCommand(args);
|
|
383
|
+
|
|
360
384
|
try {
|
|
361
385
|
const runResult = await runCommand(command, context.abort);
|
|
362
386
|
const errors: string[] = [];
|
|
@@ -449,6 +473,18 @@ export async function executeSlitherAnalyze(
|
|
|
449
473
|
}
|
|
450
474
|
}
|
|
451
475
|
|
|
476
|
+
export function detectViaIr(target: string): boolean {
|
|
477
|
+
const projectDir = target.endsWith(".sol") ? join(target, "..") : target;
|
|
478
|
+
const foundryTomlPath = join(projectDir, "foundry.toml");
|
|
479
|
+
if (!existsSync(foundryTomlPath)) return false;
|
|
480
|
+
try {
|
|
481
|
+
const content = readFileSync(foundryTomlPath, "utf-8");
|
|
482
|
+
return /^\s*via[_-]ir\s*=\s*true/m.test(content);
|
|
483
|
+
} catch {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
452
488
|
export const slitherTool = tool({
|
|
453
489
|
description:
|
|
454
490
|
"Run Slither static analysis and return normalized findings for Solidity targets.",
|
|
@@ -459,7 +495,8 @@ export const slitherTool = tool({
|
|
|
459
495
|
solc_version: tool.schema.string().optional(),
|
|
460
496
|
},
|
|
461
497
|
async execute(args, context) {
|
|
462
|
-
const
|
|
498
|
+
const viaIr = detectViaIr(args.target);
|
|
499
|
+
const result = await executeSlitherAnalyze({ ...args, via_ir: viaIr }, context);
|
|
463
500
|
return JSON.stringify(result);
|
|
464
501
|
},
|
|
465
502
|
});
|
|
@@ -7,6 +7,7 @@ export interface ProjectConfig {
|
|
|
7
7
|
testDir: string;
|
|
8
8
|
solcVersion?: string;
|
|
9
9
|
remappings: string[];
|
|
10
|
+
viaIr: boolean;
|
|
10
11
|
rootDir: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -43,6 +44,7 @@ export async function detectProject(dir: string): Promise<ProjectConfig> {
|
|
|
43
44
|
let testDir = "test";
|
|
44
45
|
let solcVersion: string | undefined;
|
|
45
46
|
let remappings: string[] = [];
|
|
47
|
+
let viaIr = false;
|
|
46
48
|
|
|
47
49
|
// Parse Foundry config if present
|
|
48
50
|
if (hasFoundry) {
|
|
@@ -51,6 +53,7 @@ export async function detectProject(dir: string): Promise<ProjectConfig> {
|
|
|
51
53
|
testDir = foundryConfig.testDir || testDir;
|
|
52
54
|
solcVersion = foundryConfig.solcVersion;
|
|
53
55
|
remappings = foundryConfig.remappings;
|
|
56
|
+
viaIr = foundryConfig.viaIr;
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
// Set Hardhat defaults if it's a Hardhat project
|
|
@@ -64,6 +67,7 @@ export async function detectProject(dir: string): Promise<ProjectConfig> {
|
|
|
64
67
|
testDir,
|
|
65
68
|
solcVersion,
|
|
66
69
|
remappings,
|
|
70
|
+
viaIr,
|
|
67
71
|
rootDir,
|
|
68
72
|
};
|
|
69
73
|
}
|
|
@@ -78,6 +82,7 @@ async function parseFoundryToml(
|
|
|
78
82
|
testDir?: string;
|
|
79
83
|
solcVersion?: string;
|
|
80
84
|
remappings: string[];
|
|
85
|
+
viaIr: boolean;
|
|
81
86
|
}> {
|
|
82
87
|
const content = await Bun.file(filePath).text();
|
|
83
88
|
|
|
@@ -86,6 +91,7 @@ async function parseFoundryToml(
|
|
|
86
91
|
testDir: undefined as string | undefined,
|
|
87
92
|
solcVersion: undefined as string | undefined,
|
|
88
93
|
remappings: [] as string[],
|
|
94
|
+
viaIr: false,
|
|
89
95
|
};
|
|
90
96
|
|
|
91
97
|
// Extract [profile.default] section - stop at next section or EOF
|
|
@@ -116,6 +122,12 @@ async function parseFoundryToml(
|
|
|
116
122
|
result.solcVersion = solcMatch[1];
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
// Parse via_ir = true/false
|
|
126
|
+
const viaIrMatch = profileSection.match(/^\s*via[_-]ir\s*=\s*(true|false)/m);
|
|
127
|
+
if (viaIrMatch && viaIrMatch[1] === "true") {
|
|
128
|
+
result.viaIr = true;
|
|
129
|
+
}
|
|
130
|
+
|
|
119
131
|
// Parse remappings array - handles both single line and multiline
|
|
120
132
|
const remappingsMatch = profileSection.match(
|
|
121
133
|
/remappings\s*=\s*\[([\s\S]*?)\]/
|