solidity-argus 0.1.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/AGENTS.md +37 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/package.json +43 -0
- package/skills/INVENTORY.md +79 -0
- package/skills/README.md +56 -0
- package/skills/checklists/cyfrin-best-practices-runtime/SKILL.md +424 -0
- package/skills/checklists/cyfrin-best-practices-upgrades/SKILL.md +157 -0
- package/skills/checklists/cyfrin-defi-core/SKILL.md +373 -0
- package/skills/checklists/cyfrin-defi-integrations/SKILL.md +412 -0
- package/skills/checklists/cyfrin-gas/SKILL.md +55 -0
- package/skills/checklists/general-audit/SKILL.md +433 -0
- package/skills/methodology/audit-workflow/SKILL.md +129 -0
- package/skills/methodology/report-template/SKILL.md +190 -0
- package/skills/methodology/severity-classification/SKILL.md +179 -0
- package/skills/protocol-patterns/amm-dex/SKILL.md +229 -0
- package/skills/protocol-patterns/bridges-cross-chain/SKILL.md +317 -0
- package/skills/protocol-patterns/dao-governance/SKILL.md +281 -0
- package/skills/protocol-patterns/lending-borrowing/SKILL.md +221 -0
- package/skills/protocol-patterns/staking-vesting/SKILL.md +247 -0
- package/skills/references/exploit-reference/SKILL.md +259 -0
- package/skills/references/smartbugs-examples/SKILL.md +296 -0
- package/skills/vulnerability-patterns/access-control/SKILL.md +298 -0
- package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +59 -0
- package/skills/vulnerability-patterns/assert-violation/SKILL.md +59 -0
- package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +61 -0
- package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +55 -0
- package/skills/vulnerability-patterns/default-visibility/SKILL.md +62 -0
- package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +60 -0
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +59 -0
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +72 -0
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +249 -0
- package/skills/vulnerability-patterns/floating-pragma/SKILL.md +51 -0
- package/skills/vulnerability-patterns/hash-collision/SKILL.md +52 -0
- package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +61 -0
- package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +60 -0
- package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +59 -0
- package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +61 -0
- package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +61 -0
- package/skills/vulnerability-patterns/logic-errors/SKILL.md +333 -0
- package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +60 -0
- package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +66 -0
- package/skills/vulnerability-patterns/off-by-one/SKILL.md +67 -0
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +252 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +65 -0
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +61 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +266 -0
- package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +72 -0
- package/skills/vulnerability-patterns/signature-malleability/SKILL.md +59 -0
- package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +63 -0
- package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +52 -0
- package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +65 -0
- package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +61 -0
- package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +63 -0
- package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +56 -0
- package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +80 -0
- package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +69 -0
- package/skills/vulnerability-patterns/unused-variables/SKILL.md +70 -0
- package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +81 -0
- package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +77 -0
- package/skills/vulnerability-patterns/weird-tokens/SKILL.md +294 -0
- package/src/agents/argus-prompt.ts +407 -0
- package/src/agents/pythia-prompt.ts +134 -0
- package/src/agents/scribe-prompt.ts +87 -0
- package/src/agents/sentinel-prompt.ts +133 -0
- package/src/cli/cli-program.ts +67 -0
- package/src/cli/commands/doctor.ts +83 -0
- package/src/cli/commands/init.ts +46 -0
- package/src/cli/commands/install.ts +55 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/tui-prompts.ts +75 -0
- package/src/cli/types.ts +9 -0
- package/src/config/index.ts +3 -0
- package/src/config/loader.ts +36 -0
- package/src/config/schema.ts +82 -0
- package/src/config/types.ts +4 -0
- package/src/constants/defaults.ts +6 -0
- package/src/create-hooks.ts +84 -0
- package/src/create-managers.ts +26 -0
- package/src/create-tools.ts +30 -0
- package/src/features/audit-enforcer/audit-enforcer.ts +34 -0
- package/src/features/audit-enforcer/index.ts +1 -0
- package/src/features/background-agent/background-manager.ts +200 -0
- package/src/features/background-agent/index.ts +1 -0
- package/src/features/context-monitor/context-monitor.ts +48 -0
- package/src/features/context-monitor/index.ts +4 -0
- package/src/features/context-monitor/tool-output-truncator.ts +17 -0
- package/src/features/error-recovery/index.ts +2 -0
- package/src/features/error-recovery/session-recovery.ts +27 -0
- package/src/features/error-recovery/tool-error-recovery.ts +35 -0
- package/src/features/index.ts +5 -0
- package/src/features/persistent-state/audit-state-manager.ts +121 -0
- package/src/features/persistent-state/index.ts +1 -0
- package/src/hooks/compaction-hook.ts +50 -0
- package/src/hooks/config-handler.ts +116 -0
- package/src/hooks/event-hook-v2.ts +93 -0
- package/src/hooks/event-hook.ts +74 -0
- package/src/hooks/hook-system.ts +9 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/knowledge-sync-hook.ts +57 -0
- package/src/hooks/safe-create-hook.ts +15 -0
- package/src/hooks/system-prompt-hook.ts +126 -0
- package/src/hooks/tool-tracking-hook.ts +234 -0
- package/src/hooks/types.ts +16 -0
- package/src/index.ts +36 -0
- package/src/knowledge/scvd-client.ts +242 -0
- package/src/knowledge/scvd-index.ts +183 -0
- package/src/knowledge/scvd-sync.ts +85 -0
- package/src/managers/index.ts +1 -0
- package/src/managers/types.ts +85 -0
- package/src/plugin-interface.ts +38 -0
- package/src/shared/binary-utils.ts +63 -0
- package/src/shared/deep-merge.ts +71 -0
- package/src/shared/file-utils.ts +56 -0
- package/src/shared/index.ts +5 -0
- package/src/shared/jsonc-parser.ts +39 -0
- package/src/shared/logger.ts +36 -0
- package/src/state/audit-state.ts +27 -0
- package/src/state/finding-store.ts +126 -0
- package/src/state/plugin-state.ts +14 -0
- package/src/state/types.ts +61 -0
- package/src/tools/contract-analyzer-tool.ts +184 -0
- package/src/tools/forge-fuzz-tool.ts +311 -0
- package/src/tools/forge-test-tool.ts +397 -0
- package/src/tools/pattern-checker-tool.ts +337 -0
- package/src/tools/report-generator-tool.ts +308 -0
- package/src/tools/slither-tool.ts +465 -0
- package/src/tools/solodit-search-tool.ts +131 -0
- package/src/tools/sync-knowledge-tool.ts +116 -0
- package/src/utils/project-detector.ts +133 -0
- package/src/utils/solidity-parser.ts +174 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { extname, join, resolve } from "node:path";
|
|
4
|
+
import { tool, type ToolContext } from "@opencode-ai/plugin";
|
|
5
|
+
import {
|
|
6
|
+
loadIndex,
|
|
7
|
+
searchIndex,
|
|
8
|
+
type ScvdIndex,
|
|
9
|
+
type ScvdIndexEntry,
|
|
10
|
+
} from "../knowledge/scvd-index";
|
|
11
|
+
|
|
12
|
+
export interface Match {
|
|
13
|
+
pattern: string;
|
|
14
|
+
severity: "Critical" | "High" | "Medium" | "Low" | "Informational";
|
|
15
|
+
file: string;
|
|
16
|
+
lines: [number, number];
|
|
17
|
+
description: string;
|
|
18
|
+
exploitReference?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MatchSource {
|
|
22
|
+
source: string;
|
|
23
|
+
matches: Match[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PatternCheckResult {
|
|
27
|
+
sources: MatchSource[];
|
|
28
|
+
patternsChecked: number;
|
|
29
|
+
executionTime: number;
|
|
30
|
+
target: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type PatternCheckArgs = {
|
|
34
|
+
target: string;
|
|
35
|
+
patterns?: string[];
|
|
36
|
+
include_scvd?: boolean;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type PatternCheckDependencies = {
|
|
40
|
+
loadIndexFn?: (filePath: string) => Promise<ScvdIndex | null>;
|
|
41
|
+
searchIndexFn?: (
|
|
42
|
+
index: ScvdIndex,
|
|
43
|
+
query: { swc?: string; severity?: string; keyword?: string; limit?: number }
|
|
44
|
+
) => ScvdIndexEntry[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type BuiltinPattern = {
|
|
48
|
+
name: string;
|
|
49
|
+
category: string;
|
|
50
|
+
severity: Match["severity"];
|
|
51
|
+
regex: RegExp;
|
|
52
|
+
description: string;
|
|
53
|
+
exploitReference?: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const BUILTIN_PATTERNS: BuiltinPattern[] = [
|
|
57
|
+
{
|
|
58
|
+
name: "reentrancy",
|
|
59
|
+
category: "reentrancy",
|
|
60
|
+
severity: "High",
|
|
61
|
+
regex: /\.call\{value:/,
|
|
62
|
+
description: "Potential reentrancy: ETH transfer via low-level call",
|
|
63
|
+
exploitReference: "DAO hack ($60M), 2016",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "tx-origin-auth",
|
|
67
|
+
category: "access-control",
|
|
68
|
+
severity: "High",
|
|
69
|
+
regex: /tx\.origin/,
|
|
70
|
+
description: "Use of tx.origin for authorization - vulnerable to phishing",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "selfdestruct",
|
|
74
|
+
category: "access-control",
|
|
75
|
+
severity: "High",
|
|
76
|
+
regex: /selfdestruct\(|suicide\(/,
|
|
77
|
+
description: "Contract uses selfdestruct - can destroy contract",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "delegatecall",
|
|
81
|
+
category: "delegatecall",
|
|
82
|
+
severity: "High",
|
|
83
|
+
regex: /\.delegatecall\(/,
|
|
84
|
+
description: "Use of delegatecall - can overwrite storage",
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "missing-zero-check",
|
|
88
|
+
category: "access-control",
|
|
89
|
+
severity: "Medium",
|
|
90
|
+
regex: /address\(0\)/,
|
|
91
|
+
description: "Potential missing zero-address validation",
|
|
92
|
+
},
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const CATEGORY_TO_SWC: Record<string, string[]> = {
|
|
96
|
+
reentrancy: ["SWC-107"],
|
|
97
|
+
"access-control": ["SWC-105", "SWC-106"],
|
|
98
|
+
"oracle-manipulation": ["SWC-116"],
|
|
99
|
+
delegatecall: ["SWC-112"],
|
|
100
|
+
"signature-replay": ["SWC-121"],
|
|
101
|
+
"integer-overflow": ["SWC-101"],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const PATTERN_NAME_TO_CATEGORY = new Map(
|
|
105
|
+
BUILTIN_PATTERNS.map((pattern) => [pattern.name, pattern.category])
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
function normalizeSeverity(value: string): Match["severity"] {
|
|
109
|
+
if (value === "Critical") return "Critical";
|
|
110
|
+
if (value === "High") return "High";
|
|
111
|
+
if (value === "Medium") return "Medium";
|
|
112
|
+
if (value === "Low") return "Low";
|
|
113
|
+
return "Informational";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function uniqueScvdEntries(entries: ScvdIndexEntry[]): ScvdIndexEntry[] {
|
|
117
|
+
const deduped = new Map<string, ScvdIndexEntry>();
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
deduped.set(entry.id, entry);
|
|
120
|
+
}
|
|
121
|
+
return Array.from(deduped.values());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function collectScvdMatches(
|
|
125
|
+
matches: Match[],
|
|
126
|
+
dependencies: Required<PatternCheckDependencies>
|
|
127
|
+
): Promise<Match[]> {
|
|
128
|
+
const detectedCategories = new Set<string>();
|
|
129
|
+
for (const match of matches) {
|
|
130
|
+
const category = PATTERN_NAME_TO_CATEGORY.get(match.pattern);
|
|
131
|
+
if (category) {
|
|
132
|
+
detectedCategories.add(category);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (detectedCategories.size === 0) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const swcCodes = new Set<string>();
|
|
141
|
+
for (const category of detectedCategories) {
|
|
142
|
+
const mappedSwcs = CATEGORY_TO_SWC[category] ?? [];
|
|
143
|
+
for (const swcCode of mappedSwcs) {
|
|
144
|
+
swcCodes.add(swcCode);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (swcCodes.size === 0) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const indexPath = join(os.homedir(), ".cache", "solidity-argus", "scvd-index.json");
|
|
153
|
+
const index = await dependencies.loadIndexFn(indexPath);
|
|
154
|
+
|
|
155
|
+
if (!index) {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const entries: ScvdIndexEntry[] = [];
|
|
160
|
+
for (const swcCode of swcCodes) {
|
|
161
|
+
entries.push(...dependencies.searchIndexFn(index, { swc: swcCode }));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return uniqueScvdEntries(entries).map((entry) => ({
|
|
165
|
+
pattern: entry.id,
|
|
166
|
+
severity: normalizeSeverity(entry.severity),
|
|
167
|
+
file: entry.repoUrl,
|
|
168
|
+
lines: [1, 1],
|
|
169
|
+
description: entry.title,
|
|
170
|
+
exploitReference: entry.repoUrl,
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function collectSolidityFiles(target: string): string[] {
|
|
175
|
+
const absoluteTarget = resolve(target);
|
|
176
|
+
let stats: ReturnType<typeof statSync>;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
stats = statSync(absoluteTarget);
|
|
180
|
+
} catch {
|
|
181
|
+
throw new Error(`Target does not exist: ${target}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (stats.isFile()) {
|
|
185
|
+
return extname(absoluteTarget) === ".sol" ? [absoluteTarget] : [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!stats.isDirectory()) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const discovered: string[] = [];
|
|
193
|
+
const stack = [absoluteTarget];
|
|
194
|
+
|
|
195
|
+
while (stack.length > 0) {
|
|
196
|
+
const current = stack.pop();
|
|
197
|
+
if (!current) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const entries = readdirSync(current, { withFileTypes: true });
|
|
202
|
+
for (const entry of entries) {
|
|
203
|
+
const fullPath = resolve(current, entry.name);
|
|
204
|
+
if (entry.isDirectory()) {
|
|
205
|
+
stack.push(fullPath);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (entry.isFile() && extname(entry.name) === ".sol") {
|
|
210
|
+
discovered.push(fullPath);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return discovered;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function lineNumberAt(content: string, index: number): number {
|
|
219
|
+
if (index <= 0) {
|
|
220
|
+
return 1;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let line = 1;
|
|
224
|
+
for (let i = 0; i < index && i < content.length; i += 1) {
|
|
225
|
+
if (content[i] === "\n") {
|
|
226
|
+
line += 1;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return line;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function lineWindow(content: string, index: number): [number, number] {
|
|
233
|
+
const linesCount = content.split("\n").length;
|
|
234
|
+
const line = lineNumberAt(content, index);
|
|
235
|
+
const start = Math.max(1, line - 5);
|
|
236
|
+
const end = Math.min(linesCount, line + 5);
|
|
237
|
+
return [start, end];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function findMatches(file: string, patterns: BuiltinPattern[]): Match[] {
|
|
241
|
+
const content = readFileSync(file, "utf8");
|
|
242
|
+
const matches: Match[] = [];
|
|
243
|
+
|
|
244
|
+
for (const pattern of patterns) {
|
|
245
|
+
const regex = new RegExp(pattern.regex.source, pattern.regex.flags.includes("g") ? pattern.regex.flags : `${pattern.regex.flags}g`);
|
|
246
|
+
for (const found of content.matchAll(regex)) {
|
|
247
|
+
const index = found.index ?? 0;
|
|
248
|
+
matches.push({
|
|
249
|
+
pattern: pattern.name,
|
|
250
|
+
severity: pattern.severity,
|
|
251
|
+
file,
|
|
252
|
+
lines: lineWindow(content, index),
|
|
253
|
+
description: pattern.description,
|
|
254
|
+
exploitReference: pattern.exploitReference,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return matches;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function selectPatterns(categories?: string[]): BuiltinPattern[] {
|
|
263
|
+
if (!categories || categories.length === 0) {
|
|
264
|
+
return BUILTIN_PATTERNS;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const set = new Set(categories);
|
|
268
|
+
return BUILTIN_PATTERNS.filter((pattern) => set.has(pattern.category));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function executePatternCheck(
|
|
272
|
+
args: PatternCheckArgs,
|
|
273
|
+
context: ToolContext,
|
|
274
|
+
deps: PatternCheckDependencies = {}
|
|
275
|
+
): Promise<PatternCheckResult> {
|
|
276
|
+
const dependencies: Required<PatternCheckDependencies> = {
|
|
277
|
+
loadIndexFn: loadIndex,
|
|
278
|
+
searchIndexFn: searchIndex,
|
|
279
|
+
...deps,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const startedAt = Date.now();
|
|
283
|
+
context.metadata({ title: `Pattern check: ${args.target}` });
|
|
284
|
+
|
|
285
|
+
const selectedPatterns = selectPatterns(args.patterns);
|
|
286
|
+
const solidityFiles = collectSolidityFiles(args.target);
|
|
287
|
+
if (solidityFiles.length === 0) {
|
|
288
|
+
throw new Error(`No Solidity files found for target: ${args.target}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const sourceMatches: Match[] = [];
|
|
292
|
+
for (const solidityFile of solidityFiles) {
|
|
293
|
+
if (context.abort.aborted) {
|
|
294
|
+
throw new Error("pattern check aborted");
|
|
295
|
+
}
|
|
296
|
+
sourceMatches.push(...findMatches(solidityFile, selectedPatterns));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const sources: MatchSource[] = [
|
|
300
|
+
{
|
|
301
|
+
source: "pattern-db",
|
|
302
|
+
matches: sourceMatches,
|
|
303
|
+
},
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
if (args.include_scvd === true) {
|
|
307
|
+
try {
|
|
308
|
+
const scvdMatches = await collectScvdMatches(sourceMatches, dependencies);
|
|
309
|
+
if (scvdMatches.length > 0) {
|
|
310
|
+
sources.push({
|
|
311
|
+
source: "scvd",
|
|
312
|
+
matches: scvdMatches,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
} catch (_e) { /* non-critical: SCVD enrichment is best-effort */ }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
sources,
|
|
320
|
+
patternsChecked: selectedPatterns.length,
|
|
321
|
+
executionTime: Date.now() - startedAt,
|
|
322
|
+
target: args.target,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export const patternCheckerTool = tool({
|
|
327
|
+
description: "Check Solidity files against deterministic vulnerability regex patterns.",
|
|
328
|
+
args: {
|
|
329
|
+
target: tool.schema.string(),
|
|
330
|
+
patterns: tool.schema.array(tool.schema.string()).optional(),
|
|
331
|
+
include_scvd: tool.schema.boolean().default(true),
|
|
332
|
+
},
|
|
333
|
+
async execute(args, context) {
|
|
334
|
+
const result = await executePatternCheck(args, context);
|
|
335
|
+
return JSON.stringify(result);
|
|
336
|
+
},
|
|
337
|
+
});
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { tool, type ToolContext } from "@opencode-ai/plugin";
|
|
2
|
+
import type { AuditState, Finding, FindingSeverity } from "../state/types";
|
|
3
|
+
|
|
4
|
+
type SeverityThreshold = "critical" | "high" | "medium" | "low" | "informational";
|
|
5
|
+
|
|
6
|
+
type ReportGeneratorArgs = {
|
|
7
|
+
project_name: string;
|
|
8
|
+
scope: string[];
|
|
9
|
+
include_executive_summary?: boolean;
|
|
10
|
+
severity_threshold?: SeverityThreshold;
|
|
11
|
+
audit_state: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type FindingsCount = {
|
|
15
|
+
critical: number;
|
|
16
|
+
high: number;
|
|
17
|
+
medium: number;
|
|
18
|
+
low: number;
|
|
19
|
+
informational: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type ReportGenerationResult = {
|
|
23
|
+
report: string;
|
|
24
|
+
findingsCount: FindingsCount;
|
|
25
|
+
filename: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const SEVERITY_ORDER: FindingSeverity[] = [
|
|
29
|
+
"Critical",
|
|
30
|
+
"High",
|
|
31
|
+
"Medium",
|
|
32
|
+
"Low",
|
|
33
|
+
"Informational",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const SEVERITY_PREFIX: Record<FindingSeverity, string> = {
|
|
37
|
+
Critical: "CRIT",
|
|
38
|
+
High: "HIGH",
|
|
39
|
+
Medium: "MED",
|
|
40
|
+
Low: "LOW",
|
|
41
|
+
Informational: "INFO",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const THRESHOLD_WEIGHT: Record<SeverityThreshold, number> = {
|
|
45
|
+
critical: 5,
|
|
46
|
+
high: 4,
|
|
47
|
+
medium: 3,
|
|
48
|
+
low: 2,
|
|
49
|
+
informational: 1,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const FINDING_WEIGHT: Record<FindingSeverity, number> = {
|
|
53
|
+
Critical: 5,
|
|
54
|
+
High: 4,
|
|
55
|
+
Medium: 3,
|
|
56
|
+
Low: 2,
|
|
57
|
+
Informational: 1,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function emptyCounts(): FindingsCount {
|
|
61
|
+
return {
|
|
62
|
+
critical: 0,
|
|
63
|
+
high: 0,
|
|
64
|
+
medium: 0,
|
|
65
|
+
low: 0,
|
|
66
|
+
informational: 0,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseAuditState(auditState: string): Finding[] {
|
|
71
|
+
let parsed: unknown;
|
|
72
|
+
try {
|
|
73
|
+
parsed = JSON.parse(auditState);
|
|
74
|
+
} catch {
|
|
75
|
+
throw new Error("audit_state is not valid JSON — expected an AuditState object or Finding[] array");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (Array.isArray(parsed)) {
|
|
79
|
+
return parsed as Finding[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof parsed === "object" && parsed !== null && Array.isArray((parsed as AuditState).findings)) {
|
|
83
|
+
return (parsed as AuditState).findings;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeTitle(check: string): string {
|
|
90
|
+
return check
|
|
91
|
+
.split(/[-_\s]+/)
|
|
92
|
+
.filter((part) => part.length > 0)
|
|
93
|
+
.map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`)
|
|
94
|
+
.join(" ");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function formatLocation(finding: Finding): string {
|
|
98
|
+
return `${finding.file}:${finding.lines[0]}-${finding.lines[1]}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function shouldIncludeFinding(finding: Finding, threshold: SeverityThreshold): boolean {
|
|
102
|
+
return FINDING_WEIGHT[finding.severity] >= THRESHOLD_WEIGHT[threshold];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function calculateCounts(findings: Finding[]): FindingsCount {
|
|
106
|
+
const counts = emptyCounts();
|
|
107
|
+
|
|
108
|
+
for (const finding of findings) {
|
|
109
|
+
if (finding.severity === "Critical") counts.critical += 1;
|
|
110
|
+
if (finding.severity === "High") counts.high += 1;
|
|
111
|
+
if (finding.severity === "Medium") counts.medium += 1;
|
|
112
|
+
if (finding.severity === "Low") counts.low += 1;
|
|
113
|
+
if (finding.severity === "Informational") counts.informational += 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return counts;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function overallRiskAssessment(counts: FindingsCount): string {
|
|
120
|
+
if (counts.critical > 0) return "Critical risk";
|
|
121
|
+
if (counts.high > 0) return "High risk";
|
|
122
|
+
if (counts.medium > 0) return "Medium risk";
|
|
123
|
+
if (counts.low > 0) return "Low risk";
|
|
124
|
+
if (counts.informational > 0) return "Informational only";
|
|
125
|
+
return "No significant risk identified";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function genericImpact(severity: FindingSeverity): string {
|
|
129
|
+
if (severity === "Critical") {
|
|
130
|
+
return "Could lead to immediate and severe compromise of funds or protocol control.";
|
|
131
|
+
}
|
|
132
|
+
if (severity === "High") {
|
|
133
|
+
return "Could materially impact protocol security, user funds, or system integrity.";
|
|
134
|
+
}
|
|
135
|
+
if (severity === "Medium") {
|
|
136
|
+
return "Could cause operational issues or increase exploitability under specific conditions.";
|
|
137
|
+
}
|
|
138
|
+
if (severity === "Low") {
|
|
139
|
+
return "Limited direct impact but should be addressed to improve security posture.";
|
|
140
|
+
}
|
|
141
|
+
return "No immediate exploit impact, but useful for hardening and maintainability.";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function genericRecommendation(severity: FindingSeverity): string {
|
|
145
|
+
if (severity === "Critical" || severity === "High") {
|
|
146
|
+
return "Prioritize remediation before production deployment and validate with focused regression tests.";
|
|
147
|
+
}
|
|
148
|
+
if (severity === "Medium") {
|
|
149
|
+
return "Address in the near term and include unit/integration tests to prevent regressions.";
|
|
150
|
+
}
|
|
151
|
+
if (severity === "Low") {
|
|
152
|
+
return "Schedule remediation in regular hardening cycles.";
|
|
153
|
+
}
|
|
154
|
+
return "Track and resolve during routine code quality and documentation improvements.";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildRecommendations(counts: FindingsCount): string[] {
|
|
158
|
+
const items: string[] = [];
|
|
159
|
+
|
|
160
|
+
if (counts.critical > 0) {
|
|
161
|
+
items.push("1. Immediately remediate all Critical findings and block release until fixes are verified.");
|
|
162
|
+
}
|
|
163
|
+
if (counts.high > 0) {
|
|
164
|
+
items.push("2. Prioritize High findings in the next patch cycle with dedicated security test coverage.");
|
|
165
|
+
}
|
|
166
|
+
if (counts.medium > 0) {
|
|
167
|
+
items.push("3. Resolve Medium findings to reduce attack surface and improve resilience.");
|
|
168
|
+
}
|
|
169
|
+
if (counts.low > 0 || counts.informational > 0) {
|
|
170
|
+
items.push("4. Address Low/Informational findings as part of ongoing hardening and code quality efforts.");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (items.length === 0) {
|
|
174
|
+
items.push("1. Maintain current controls, monitor code changes, and re-audit before major upgrades.");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return items;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function buildFindingsSection(findings: Finding[]): string {
|
|
181
|
+
if (findings.length === 0) {
|
|
182
|
+
return "## Findings\nNo findings meet the configured severity threshold.";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const lines: string[] = ["## Findings"];
|
|
186
|
+
|
|
187
|
+
for (const severity of SEVERITY_ORDER) {
|
|
188
|
+
const severityFindings = findings.filter((finding) => finding.severity === severity);
|
|
189
|
+
if (severityFindings.length === 0) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
lines.push(`### ${severity}`);
|
|
194
|
+
|
|
195
|
+
severityFindings.forEach((finding, index) => {
|
|
196
|
+
const prefix = SEVERITY_PREFIX[severity];
|
|
197
|
+
const findingId = `[${prefix}-${index + 1}]`;
|
|
198
|
+
const title = normalizeTitle(finding.check);
|
|
199
|
+
const recommendation = finding.remediation ?? genericRecommendation(severity);
|
|
200
|
+
|
|
201
|
+
lines.push(`### ${findingId} ${title}`);
|
|
202
|
+
lines.push(`**Severity**: ${finding.severity}`);
|
|
203
|
+
lines.push(`**Confidence**: ${finding.confidence}`);
|
|
204
|
+
lines.push(`**Location**: ${formatLocation(finding)}`);
|
|
205
|
+
lines.push("");
|
|
206
|
+
lines.push(`**Description**: ${finding.description}`);
|
|
207
|
+
lines.push("");
|
|
208
|
+
lines.push(`**Impact**: ${genericImpact(finding.severity)}`);
|
|
209
|
+
lines.push("");
|
|
210
|
+
lines.push(`**Recommendation**: ${recommendation}`);
|
|
211
|
+
lines.push("");
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return lines.join("\n");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function executeReportGeneration(
|
|
219
|
+
args: ReportGeneratorArgs,
|
|
220
|
+
context: ToolContext
|
|
221
|
+
): Promise<ReportGenerationResult> {
|
|
222
|
+
const includeExecutiveSummary = args.include_executive_summary ?? true;
|
|
223
|
+
const threshold = args.severity_threshold ?? "low";
|
|
224
|
+
const findings = parseAuditState(args.audit_state).filter((finding) =>
|
|
225
|
+
shouldIncludeFinding(finding, threshold)
|
|
226
|
+
);
|
|
227
|
+
const counts = calculateCounts(findings);
|
|
228
|
+
const auditDate = new Date().toISOString().slice(0, 10);
|
|
229
|
+
|
|
230
|
+
context.metadata({ title: `Generate audit report: ${args.project_name}` });
|
|
231
|
+
|
|
232
|
+
const sections: string[] = [`# Security Audit Report — ${args.project_name}`];
|
|
233
|
+
|
|
234
|
+
if (includeExecutiveSummary) {
|
|
235
|
+
sections.push("## Executive Summary");
|
|
236
|
+
sections.push(
|
|
237
|
+
`This report summarizes security findings identified for ${args.project_name} based on static analysis, testing, and pattern-based review.`
|
|
238
|
+
);
|
|
239
|
+
sections.push("");
|
|
240
|
+
sections.push("| Severity | Count |");
|
|
241
|
+
sections.push("| --- | ---: |");
|
|
242
|
+
sections.push(`| Critical | ${counts.critical} |`);
|
|
243
|
+
sections.push(`| High | ${counts.high} |`);
|
|
244
|
+
sections.push(`| Medium | ${counts.medium} |`);
|
|
245
|
+
sections.push(`| Low | ${counts.low} |`);
|
|
246
|
+
sections.push(`| Informational | ${counts.informational} |`);
|
|
247
|
+
sections.push("");
|
|
248
|
+
sections.push(`Overall risk assessment: ${overallRiskAssessment(counts)}.`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
sections.push("## Scope");
|
|
252
|
+
sections.push("Contracts in scope:");
|
|
253
|
+
if (args.scope.length === 0) {
|
|
254
|
+
sections.push("- None provided");
|
|
255
|
+
} else {
|
|
256
|
+
for (const contract of args.scope) {
|
|
257
|
+
sections.push(`- ${contract}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
sections.push(`Audit date: ${auditDate}`);
|
|
261
|
+
|
|
262
|
+
sections.push("## Methodology");
|
|
263
|
+
sections.push("Tools and techniques used:");
|
|
264
|
+
sections.push("- Slither static analysis");
|
|
265
|
+
sections.push("- Foundry tests and fuzzing");
|
|
266
|
+
sections.push("- Pattern Analysis");
|
|
267
|
+
sections.push("- Solodit research cross-referencing");
|
|
268
|
+
sections.push(
|
|
269
|
+
"Approach: Findings were normalized, deduplicated by detector signature and location, then prioritized by severity and confidence."
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
sections.push(buildFindingsSection(findings));
|
|
273
|
+
|
|
274
|
+
sections.push("## Recommendations");
|
|
275
|
+
for (const item of buildRecommendations(counts)) {
|
|
276
|
+
sections.push(`- ${item}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
sections.push("## Appendix");
|
|
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}`);
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
report: sections.join("\n\n"),
|
|
287
|
+
findingsCount: counts,
|
|
288
|
+
filename: `${args.project_name}-audit-report-${auditDate}.md`,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export const reportGeneratorTool = tool({
|
|
293
|
+
description:
|
|
294
|
+
"Generate a professional markdown security audit report from serialized findings and audit context.",
|
|
295
|
+
args: {
|
|
296
|
+
project_name: tool.schema.string(),
|
|
297
|
+
scope: tool.schema.array(tool.schema.string()),
|
|
298
|
+
include_executive_summary: tool.schema.boolean().default(true),
|
|
299
|
+
severity_threshold: tool.schema
|
|
300
|
+
.enum(["critical", "high", "medium", "low", "informational"])
|
|
301
|
+
.default("low"),
|
|
302
|
+
audit_state: tool.schema.string(),
|
|
303
|
+
},
|
|
304
|
+
async execute(args, context) {
|
|
305
|
+
const result = await executeReportGeneration(args, context);
|
|
306
|
+
return JSON.stringify(result);
|
|
307
|
+
},
|
|
308
|
+
});
|