anchor-audit 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/README.md +28 -0
- package/dist/auditor.d.ts +16 -0
- package/dist/auditor.js +235 -0
- package/dist/auditor.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +96 -0
- package/dist/index.js.map +1 -0
- package/dist/metadata.d.ts +25 -0
- package/dist/metadata.js +48 -0
- package/dist/metadata.js.map +1 -0
- package/dist/reporter.d.ts +18 -0
- package/dist/reporter.js +177 -0
- package/dist/reporter.js.map +1 -0
- package/dist/rules-loader.d.ts +12 -0
- package/dist/rules-loader.js +65 -0
- package/dist/rules-loader.js.map +1 -0
- package/dist/scanner.d.ts +6 -0
- package/dist/scanner.js +42 -0
- package/dist/scanner.js.map +1 -0
- package/package.json +41 -0
- package/rules/001-missing-signer-check.md +57 -0
- package/rules/002-missing-owner-check.md +53 -0
- package/rules/003-missing-discriminator-check.md +53 -0
- package/rules/004-account-substitution.md +54 -0
- package/rules/005-sysvar-spoofing.md +57 -0
- package/rules/006-missing-rent-exemption-check.md +47 -0
- package/rules/007-account-aliasing.md +53 -0
- package/rules/008-uninitialized-account-use.md +53 -0
- package/rules/009-missing-mut-constraint.md +52 -0
- package/rules/010-missing-close-constraint.md +48 -0
- package/rules/011-pda-seed-collision.md +49 -0
- package/rules/012-missing-bump-validation.md +55 -0
- package/rules/013-non-canonical-bump-accepted.md +52 -0
- package/rules/014-predictable-pda.md +57 -0
- package/rules/015-insecure-pda-across-upgrades.md +54 -0
- package/rules/016-bump-mismatch.md +49 -0
- package/rules/017-arbitrary-cpi.md +56 -0
- package/rules/018-cpi-confused-deputy.md +50 -0
- package/rules/019-missing-program-id-check-spl.md +51 -0
- package/rules/020-reentrancy-via-cpi.md +50 -0
- package/rules/021-untrusted-callback.md +49 -0
- package/rules/022-cpi-with-attacker-accounts.md +58 -0
- package/rules/023-lamport-overflow.md +50 -0
- package/rules/024-token-amount-overflow.md +52 -0
- package/rules/025-precision-loss.md +42 -0
- package/rules/026-rounding-direction.md +43 -0
- package/rules/027-token-decimal-mismatch.md +50 -0
- package/rules/028-integer-cast-truncation.md +42 -0
- package/rules/029-off-by-one.md +45 -0
- package/rules/030-missing-authorization.md +51 -0
- package/rules/031-reinitialization-attack.md +50 -0
- package/rules/032-closed-account-revival.md +49 -0
- package/rules/033-init-if-needed-misuse.md +66 -0
- package/rules/034-missing-has-one.md +55 -0
- package/rules/035-insecure-admin-transfer.md +49 -0
- package/rules/036-missing-pause-guards.md +50 -0
- package/rules/037-clock-manipulation.md +53 -0
- package/rules/038-missing-address-validation.md +53 -0
- package/rules/039-constraint-evaluation-stage.md +54 -0
- package/rules/040-realloc-zero-init.md +53 -0
- package/rules/041-missing-payer-on-init.md +59 -0
- package/rules/042-incorrect-space-allocation.md +53 -0
- package/rules/043-account-vs-account-info.md +53 -0
- package/rules/044-token-account-owner-unverified.md +56 -0
- package/rules/045-token-mint-unverified.md +58 -0
- package/rules/046-ata-assumption-errors.md +53 -0
- package/rules/047-token-program-id-hardcoded.md +55 -0
- package/rules/048-compute-budget-abuse.md +51 -0
- package/rules/049-log-spam-dos.md +49 -0
- package/rules/050-stack-overflow-deep-cpi.md +48 -0
- package/rules/INDEX.md +65 -0
- package/rules/README.md +40 -0
package/dist/reporter.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render the audit report as markdown (default) or JSON.
|
|
3
|
+
*
|
|
4
|
+
* Auto-save behaviour (always on):
|
|
5
|
+
* Every run writes a timestamped file to reports/<project>-<timestamp>.<ext>
|
|
6
|
+
* so no audit is ever lost. Pass --output to additionally write to a
|
|
7
|
+
* specific path. Stdout is printed when --output is not supplied.
|
|
8
|
+
*
|
|
9
|
+
* JSON output schema
|
|
10
|
+
* ------------------
|
|
11
|
+
* {
|
|
12
|
+
* "version": "0.1.0",
|
|
13
|
+
* "date": "YYYY-MM-DD",
|
|
14
|
+
* "metadata": { ...AuditMetadata },
|
|
15
|
+
* "summary": { "critical": N, "high": N, "medium": N, "low": N, "total": N },
|
|
16
|
+
* "findings": [ ...Finding[] ]
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import chalk from "chalk";
|
|
22
|
+
import { SEVERITY_ORDER } from "./auditor.js";
|
|
23
|
+
const VERSION = "0.1.0";
|
|
24
|
+
export function countBySeverity(findings) {
|
|
25
|
+
const c = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
26
|
+
for (const f of findings)
|
|
27
|
+
c[f.severity]++;
|
|
28
|
+
return c;
|
|
29
|
+
}
|
|
30
|
+
function formatDuration(ms) {
|
|
31
|
+
if (ms < 1000)
|
|
32
|
+
return `${ms}ms`;
|
|
33
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
34
|
+
}
|
|
35
|
+
export function renderJson(findings, meta) {
|
|
36
|
+
const counts = countBySeverity(findings);
|
|
37
|
+
const report = {
|
|
38
|
+
version: VERSION,
|
|
39
|
+
date: new Date().toISOString().slice(0, 10),
|
|
40
|
+
...(meta ? { metadata: meta } : {}),
|
|
41
|
+
summary: { ...counts, total: findings.length },
|
|
42
|
+
findings,
|
|
43
|
+
};
|
|
44
|
+
return JSON.stringify(report, null, 2);
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Markdown output
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
const SEV_LABEL = {
|
|
50
|
+
critical: "CRITICAL",
|
|
51
|
+
high: "HIGH",
|
|
52
|
+
medium: "MEDIUM",
|
|
53
|
+
low: "LOW",
|
|
54
|
+
};
|
|
55
|
+
function renderMetadataBlock(meta) {
|
|
56
|
+
const rows = [
|
|
57
|
+
["Date", meta.date],
|
|
58
|
+
["Time", meta.time],
|
|
59
|
+
["Model", meta.model],
|
|
60
|
+
["Provider", meta.provider],
|
|
61
|
+
["Effort", meta.effort],
|
|
62
|
+
["CLI Version", `v${meta.cliVersion}`],
|
|
63
|
+
["Project", meta.project],
|
|
64
|
+
];
|
|
65
|
+
if (meta.gitBranch)
|
|
66
|
+
rows.push(["Git Branch", meta.gitBranch]);
|
|
67
|
+
if (meta.gitCommit)
|
|
68
|
+
rows.push(["Git Commit", meta.gitCommit]);
|
|
69
|
+
rows.push(["OS", meta.os]);
|
|
70
|
+
rows.push(["Duration", formatDuration(meta.durationMs)]);
|
|
71
|
+
rows.push(["Files Analyzed", String(meta.totalFiles)]);
|
|
72
|
+
rows.push(["Total Findings", String(meta.totalFindings)]);
|
|
73
|
+
const lines = ["## Audit Metadata", "", "| Field | Value |", "|-------|-------|"];
|
|
74
|
+
for (const [k, v] of rows)
|
|
75
|
+
lines.push(`| ${k} | ${v} |`);
|
|
76
|
+
return lines.join("\n");
|
|
77
|
+
}
|
|
78
|
+
export function renderMarkdown(findings, meta) {
|
|
79
|
+
const date = meta?.date ?? new Date().toISOString().slice(0, 10);
|
|
80
|
+
const counts = countBySeverity(findings);
|
|
81
|
+
const lines = [];
|
|
82
|
+
lines.push(`# Anchor Audit Report`);
|
|
83
|
+
lines.push(`Generated by anchor-audit v${VERSION} on ${date}`);
|
|
84
|
+
lines.push("");
|
|
85
|
+
if (meta) {
|
|
86
|
+
lines.push(renderMetadataBlock(meta));
|
|
87
|
+
lines.push("");
|
|
88
|
+
}
|
|
89
|
+
lines.push("## Summary");
|
|
90
|
+
lines.push("");
|
|
91
|
+
lines.push("| Severity | Count |");
|
|
92
|
+
lines.push("|----------|-------|");
|
|
93
|
+
for (const sev of ["critical", "high", "medium", "low"]) {
|
|
94
|
+
lines.push(`| ${sev.charAt(0).toUpperCase() + sev.slice(1).padEnd(7)} | ${counts[sev]} |`);
|
|
95
|
+
}
|
|
96
|
+
lines.push(`| **Total** | **${findings.length}** |`);
|
|
97
|
+
lines.push("");
|
|
98
|
+
if (findings.length === 0) {
|
|
99
|
+
lines.push("> No findings. Always verify manually before deploying to mainnet.");
|
|
100
|
+
return lines.join("\n");
|
|
101
|
+
}
|
|
102
|
+
lines.push("## Findings");
|
|
103
|
+
lines.push("");
|
|
104
|
+
const bySev = ["critical", "high", "medium", "low"].flatMap((s) => findings.filter((f) => f.severity === s));
|
|
105
|
+
for (const f of bySev) {
|
|
106
|
+
const loc = f.line != null ? `${f.file}:${f.line}` : f.file;
|
|
107
|
+
lines.push(`### [${SEV_LABEL[f.severity]}] Rule ${f.ruleId}: ${f.title}`);
|
|
108
|
+
lines.push("");
|
|
109
|
+
lines.push(`**File:** \`${loc}\``);
|
|
110
|
+
lines.push("");
|
|
111
|
+
lines.push(`**Description:** ${f.description}`);
|
|
112
|
+
lines.push("");
|
|
113
|
+
if (f.vulnerableCode.trim()) {
|
|
114
|
+
lines.push("**Vulnerable code:**");
|
|
115
|
+
lines.push("```rust");
|
|
116
|
+
lines.push(f.vulnerableCode.trim());
|
|
117
|
+
lines.push("```");
|
|
118
|
+
lines.push("");
|
|
119
|
+
}
|
|
120
|
+
lines.push(`**Recommendation:** ${f.recommendation}`);
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push(`**Reference:** [rules/${f.ruleId}-${f.ruleName}.md](rules/${f.ruleId}-${f.ruleName}.md)`);
|
|
123
|
+
lines.push("");
|
|
124
|
+
lines.push("---");
|
|
125
|
+
lines.push("");
|
|
126
|
+
}
|
|
127
|
+
return lines.join("\n");
|
|
128
|
+
}
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Auto-save to reports/
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
function autoSavePath(project, format) {
|
|
133
|
+
const ext = format === "json" ? "json" : "md";
|
|
134
|
+
// Replace colons with dashes for filesystem-safe filenames.
|
|
135
|
+
const ts = new Date().toISOString().replace(/:/g, "-").replace(/\..+$/, "Z");
|
|
136
|
+
return join("reports", `${project}-${ts}.${ext}`);
|
|
137
|
+
}
|
|
138
|
+
function writeAutoSave(content, project, format) {
|
|
139
|
+
mkdirSync("reports", { recursive: true });
|
|
140
|
+
const savePath = autoSavePath(project, format);
|
|
141
|
+
writeFileSync(savePath, content, "utf8");
|
|
142
|
+
return savePath;
|
|
143
|
+
}
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Entry point
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
export async function renderReport(findings, options, meta) {
|
|
148
|
+
const output = options.format === "json"
|
|
149
|
+
? renderJson(findings, meta)
|
|
150
|
+
: renderMarkdown(findings, meta);
|
|
151
|
+
const project = meta?.project ?? "audit";
|
|
152
|
+
// Always auto-save a timestamped copy to reports/.
|
|
153
|
+
const savedPath = writeAutoSave(output, project, options.format);
|
|
154
|
+
if (options.output) {
|
|
155
|
+
writeFileSync(options.output, output, "utf8");
|
|
156
|
+
}
|
|
157
|
+
const counts = countBySeverity(findings);
|
|
158
|
+
const label = counts.critical > 0
|
|
159
|
+
? chalk.red(`${counts.critical} critical`)
|
|
160
|
+
: counts.high > 0
|
|
161
|
+
? chalk.yellow(`${counts.high} high`)
|
|
162
|
+
: chalk.green("clean");
|
|
163
|
+
process.stderr.write(`Saved report → ${savedPath}` +
|
|
164
|
+
(options.output ? ` (also → ${options.output})` : "") +
|
|
165
|
+
` — ${label}, ${findings.length} total finding(s)\n`);
|
|
166
|
+
// Print to stdout when no --output flag (terminal viewing).
|
|
167
|
+
if (!options.output) {
|
|
168
|
+
process.stdout.write(output + "\n");
|
|
169
|
+
}
|
|
170
|
+
// Exit code 1 when any critical or high finding is present.
|
|
171
|
+
if (counts.critical > 0 || counts.high > 0) {
|
|
172
|
+
process.exitCode = 1;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Re-export SEVERITY_ORDER so tests can use it without importing from auditor
|
|
176
|
+
export { SEVERITY_ORDER };
|
|
177
|
+
//# sourceMappingURL=reporter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reporter.js","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Severity } from "./index.js";
|
|
2
|
+
export interface Rule {
|
|
3
|
+
/** Zero-padded three-digit ID, e.g. "001". */
|
|
4
|
+
id: string;
|
|
5
|
+
/** Slug from the filename, e.g. "missing-signer-check". */
|
|
6
|
+
name: string;
|
|
7
|
+
severity: Severity;
|
|
8
|
+
category: string;
|
|
9
|
+
/** Full markdown content — sent to the model as audit context. */
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function loadRules(ruleIdsArg?: string): Promise<Rule[]>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads the rule catalog from the `rules/` directory and parses each rule's
|
|
3
|
+
* ID, title, severity, and category from its markdown.
|
|
4
|
+
*
|
|
5
|
+
* Resolution order (first that exists wins):
|
|
6
|
+
* 1. repo layout — <dist>/../../rules (cli/dist/../../rules)
|
|
7
|
+
* 2. npm install — <dist>/../rules (dist/../rules)
|
|
8
|
+
*/
|
|
9
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
function findRulesDir() {
|
|
13
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const candidates = [
|
|
15
|
+
join(here, "..", "..", "rules"), // repo: cli/dist/../../rules
|
|
16
|
+
join(here, "..", "rules"), // installed: cli/dist/../rules
|
|
17
|
+
];
|
|
18
|
+
for (const c of candidates) {
|
|
19
|
+
if (existsSync(join(c, "INDEX.md")))
|
|
20
|
+
return c;
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`Cannot locate rules/ directory. Tried:\n` +
|
|
23
|
+
candidates.map((c) => ` ${c}`).join("\n") +
|
|
24
|
+
`\nRun anchor-audit from the repo root or reinstall the package.`);
|
|
25
|
+
}
|
|
26
|
+
function parseSeverity(content, file) {
|
|
27
|
+
const m = content.match(/\*\*Severity:\*\*\s*(Critical|High|Medium|Low)/i);
|
|
28
|
+
if (!m?.[1])
|
|
29
|
+
throw new Error(`No **Severity:** line found in ${file}`);
|
|
30
|
+
return m[1].toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
function parseCategory(content) {
|
|
33
|
+
const m = content.match(/\*\*Category:\*\*\s*(.+)/);
|
|
34
|
+
return m?.[1]?.trim() ?? "Unknown";
|
|
35
|
+
}
|
|
36
|
+
export async function loadRules(ruleIdsArg) {
|
|
37
|
+
const rulesDir = findRulesDir();
|
|
38
|
+
const requested = ruleIdsArg
|
|
39
|
+
? new Set(ruleIdsArg.split(",").map((s) => s.trim().padStart(3, "0")))
|
|
40
|
+
: null;
|
|
41
|
+
const files = readdirSync(rulesDir)
|
|
42
|
+
.filter((f) => /^\d{3}-.+\.md$/.test(f))
|
|
43
|
+
.sort();
|
|
44
|
+
const rules = [];
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const id = file.slice(0, 3);
|
|
47
|
+
if (requested && !requested.has(id))
|
|
48
|
+
continue;
|
|
49
|
+
const content = readFileSync(join(rulesDir, file), "utf8");
|
|
50
|
+
rules.push({
|
|
51
|
+
id,
|
|
52
|
+
name: file.slice(4, -3), // strip "NNN-" prefix and ".md" suffix
|
|
53
|
+
severity: parseSeverity(content, file),
|
|
54
|
+
category: parseCategory(content),
|
|
55
|
+
content,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (rules.length === 0) {
|
|
59
|
+
throw new Error(requested
|
|
60
|
+
? `No rules found for IDs: ${ruleIdsArg}`
|
|
61
|
+
: `No rule files found in ${rulesDir}`);
|
|
62
|
+
}
|
|
63
|
+
return rules;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=rules-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules-loader.js","sourceRoot":"","sources":["../src/rules-loader.ts"],"names":[],"mappings":"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"}
|
package/dist/scanner.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File collection for the audit target.
|
|
3
|
+
*
|
|
4
|
+
* Recursively walks the target directory, collects every `.rs` source file,
|
|
5
|
+
* and warns (but does not fail) when the layout doesn't look like an Anchor
|
|
6
|
+
* program (no `lib.rs` found).
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
9
|
+
import { join, relative } from "node:path";
|
|
10
|
+
const SKIP_DIRS = new Set(["target", "node_modules", ".git", ".anchor"]);
|
|
11
|
+
export async function scanProgram(targetPath) {
|
|
12
|
+
if (!existsSync(targetPath)) {
|
|
13
|
+
throw new Error(`Target path does not exist: ${targetPath}`);
|
|
14
|
+
}
|
|
15
|
+
if (!statSync(targetPath).isDirectory()) {
|
|
16
|
+
throw new Error(`Target must be a directory: ${targetPath}`);
|
|
17
|
+
}
|
|
18
|
+
const files = [];
|
|
19
|
+
collectRs(targetPath, targetPath, files);
|
|
20
|
+
if (files.length === 0) {
|
|
21
|
+
throw new Error(`No .rs source files found under: ${targetPath}`);
|
|
22
|
+
}
|
|
23
|
+
const hasLibRs = files.some((f) => f.path.endsWith("lib.rs"));
|
|
24
|
+
if (!hasLibRs) {
|
|
25
|
+
process.stderr.write(`warning: no lib.rs found under ${targetPath} — may not be an Anchor program\n`);
|
|
26
|
+
}
|
|
27
|
+
return files;
|
|
28
|
+
}
|
|
29
|
+
function collectRs(base, dir, out) {
|
|
30
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
31
|
+
if (entry.isDirectory()) {
|
|
32
|
+
if (SKIP_DIRS.has(entry.name))
|
|
33
|
+
continue;
|
|
34
|
+
collectRs(base, join(dir, entry.name), out);
|
|
35
|
+
}
|
|
36
|
+
else if (entry.isFile() && entry.name.endsWith(".rs")) {
|
|
37
|
+
const full = join(dir, entry.name);
|
|
38
|
+
out.push({ path: relative(base, full), content: readFileSync(full, "utf8") });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=scanner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scanner.js","sourceRoot":"","sources":["../src/scanner.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAQ3C,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,cAAc,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;AAEzE,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,UAAkB;IAClD,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,+BAA+B,UAAU,EAAE,CAAC,CAAC;IAC/D,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,+BAA+B,UAAU,EAAE,CAAC,CAAC;IAC/D,CAAC;IAED,MAAM,KAAK,GAAiB,EAAE,CAAC;IAC/B,SAAS,CAAC,UAAU,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC;IAEzC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,oCAAoC,UAAU,EAAE,CAAC,CAAC;IACpE,CAAC;IAED,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC9D,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,kCAAkC,UAAU,mCAAmC,CAChF,CAAC;IACJ,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,SAAS,CAAC,IAAY,EAAE,GAAW,EAAE,GAAiB;IAC7D,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;QAC9D,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YACxB,IAAI,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,SAAS;YACxC,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;QAC9C,CAAC;aAAM,IAAI,KAAK,CAAC,MAAM,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACxD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YACnC,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;QAChF,CAAC;IACH,CAAC;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "anchor-audit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI-driven security audit CLI for Anchor programs on Solana",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"anchor-audit": "dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"rules"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json",
|
|
19
|
+
"prepublishOnly": "rm -rf ./rules && cp -r ../rules ./rules && npm run build",
|
|
20
|
+
"postpublish": "rm -rf ./rules"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"solana",
|
|
24
|
+
"anchor",
|
|
25
|
+
"security",
|
|
26
|
+
"audit",
|
|
27
|
+
"claude",
|
|
28
|
+
"smart-contracts"
|
|
29
|
+
],
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@anthropic-ai/sdk": "^0.39.0",
|
|
32
|
+
"chalk": "^5.4.0",
|
|
33
|
+
"commander": "^13.0.0",
|
|
34
|
+
"dotenv": "^17.4.2",
|
|
35
|
+
"openai": "^4.77.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^22.10.0",
|
|
39
|
+
"typescript": "^5.7.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Rule 001: Missing Signer Check
|
|
2
|
+
|
|
3
|
+
**Severity:** Critical
|
|
4
|
+
**Category:** Account validation
|
|
5
|
+
|
|
6
|
+
## Description
|
|
7
|
+
An instruction that performs a privileged action (changing an authority, moving funds, mutating user state) accepts the relevant authority account without requiring its signature. Comparing the account's public key against a stored authority proves the *address* matches, but not that the holder of that key approved the transaction — anyone can pass any public key as a read-only account.
|
|
8
|
+
|
|
9
|
+
## Vulnerable pattern
|
|
10
|
+
```rust
|
|
11
|
+
#[derive(Accounts)]
|
|
12
|
+
pub struct UpdateAuthority<'info> {
|
|
13
|
+
#[account(mut)]
|
|
14
|
+
pub vault: Account<'info, Vault>,
|
|
15
|
+
/// CHECK: current vault authority
|
|
16
|
+
pub authority: AccountInfo<'info>,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pub fn update_authority(ctx: Context<UpdateAuthority>, new_authority: Pubkey) -> Result<()> {
|
|
20
|
+
// Key equality only — no signature required
|
|
21
|
+
require_keys_eq!(ctx.accounts.vault.authority, ctx.accounts.authority.key());
|
|
22
|
+
ctx.accounts.vault.authority = new_authority;
|
|
23
|
+
Ok(())
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Why this is dangerous
|
|
28
|
+
An attacker submits the transaction themselves, passing the legitimate authority's public key as the `authority` account without its signature. The key-equality check passes, and the attacker rotates the vault authority to a key they control, then drains the vault through legitimate paths. This is the single most common Solana vulnerability class.
|
|
29
|
+
|
|
30
|
+
## Fix pattern
|
|
31
|
+
```rust
|
|
32
|
+
#[derive(Accounts)]
|
|
33
|
+
pub struct UpdateAuthority<'info> {
|
|
34
|
+
#[account(mut, has_one = authority)]
|
|
35
|
+
pub vault: Account<'info, Vault>,
|
|
36
|
+
pub authority: Signer<'info>, // Anchor enforces is_signer
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
pub fn update_authority(ctx: Context<UpdateAuthority>, new_authority: Pubkey) -> Result<()> {
|
|
40
|
+
ctx.accounts.vault.authority = new_authority;
|
|
41
|
+
Ok(())
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Detection heuristic
|
|
46
|
+
- Accounts named `authority`, `admin`, `owner`, `signer`, `user`, or `payer` typed as `AccountInfo` or `UncheckedAccount` instead of `Signer`
|
|
47
|
+
- Handlers that mutate state or move lamports/tokens where no account in the context is a `Signer`
|
|
48
|
+
- Key comparisons (`require_keys_eq!`, `==` on `key()`) against a stored authority with no accompanying signature requirement
|
|
49
|
+
- In non-Anchor code paths: missing `if !account.is_signer { return Err(...) }`
|
|
50
|
+
|
|
51
|
+
## References
|
|
52
|
+
- Neodyme — Solana common pitfalls: missing signer check (https://neodyme.io/en/blog/solana_common_pitfalls/)
|
|
53
|
+
- Coral sealevel-attacks — 0-signer-authorization (https://github.com/coral-xyz/sealevel-attacks/tree/master/programs/0-signer-authorization)
|
|
54
|
+
- Solana program security course — signer authorization (https://solana.com/developers/courses/program-security/signer-auth)
|
|
55
|
+
|
|
56
|
+
## Real-world exploits (if any)
|
|
57
|
+
No single headline exploit; missing signer checks appear repeatedly in public audit reports (OtterSec, Neodyme, Sec3) as critical findings caught pre-deployment.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Rule 002: Missing Owner Check
|
|
2
|
+
|
|
3
|
+
**Severity:** Critical
|
|
4
|
+
**Category:** Account validation
|
|
5
|
+
|
|
6
|
+
## Description
|
|
7
|
+
An account is read or trusted without verifying which program owns it. On Solana, only an account's owner program can modify its data, but *anyone* can create an account with arbitrary contents owned by a different program (or the System Program) and pass it into your instruction. If the program deserializes account data without an owner check, an attacker can substitute a forged account with attacker-chosen field values.
|
|
8
|
+
|
|
9
|
+
## Vulnerable pattern
|
|
10
|
+
```rust
|
|
11
|
+
#[derive(Accounts)]
|
|
12
|
+
pub struct Withdraw<'info> {
|
|
13
|
+
/// CHECK: config account
|
|
14
|
+
pub config: AccountInfo<'info>,
|
|
15
|
+
pub admin: Signer<'info>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
|
|
19
|
+
// Deserializes raw bytes — never checks config.owner
|
|
20
|
+
let config = Config::try_from_slice(&ctx.accounts.config.data.borrow())?;
|
|
21
|
+
require_keys_eq!(config.admin, ctx.accounts.admin.key());
|
|
22
|
+
// ... transfer `amount` out
|
|
23
|
+
Ok(())
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Why this is dangerous
|
|
28
|
+
The attacker creates their own account with the same byte layout as `Config`, sets `admin` to their own key, and passes it in. The deserialization succeeds, the admin check passes against the forged data, and the attacker withdraws funds. Every field read from an unverified account is attacker-controlled.
|
|
29
|
+
|
|
30
|
+
## Fix pattern
|
|
31
|
+
```rust
|
|
32
|
+
#[derive(Accounts)]
|
|
33
|
+
pub struct Withdraw<'info> {
|
|
34
|
+
// Account<'info, T> verifies owner == crate::ID and the discriminator
|
|
35
|
+
pub config: Account<'info, Config>,
|
|
36
|
+
pub admin: Signer<'info>,
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
For accounts owned by another program, constrain explicitly: `#[account(owner = other_program::ID)]`.
|
|
40
|
+
|
|
41
|
+
## Detection heuristic
|
|
42
|
+
- `AccountInfo` / `UncheckedAccount` whose `.data` is borrowed and deserialized (`try_from_slice`, `AnchorDeserialize`, manual byte slicing)
|
|
43
|
+
- No `owner =` constraint and no `require_keys_eq!(account.owner, ...)` before trusting the data
|
|
44
|
+
- `/// CHECK:` comments that do not explain a real validation performed elsewhere
|
|
45
|
+
- Raw-Solana handlers missing `if account.owner != program_id` before reads
|
|
46
|
+
|
|
47
|
+
## References
|
|
48
|
+
- Neodyme — Solana common pitfalls: missing ownership check (https://neodyme.io/en/blog/solana_common_pitfalls/)
|
|
49
|
+
- Coral sealevel-attacks — 2-owner-checks (https://github.com/coral-xyz/sealevel-attacks/tree/master/programs/2-owner-checks)
|
|
50
|
+
- Solana program security course — owner checks (https://solana.com/developers/courses/program-security/owner-checks)
|
|
51
|
+
|
|
52
|
+
## Real-world exploits (if any)
|
|
53
|
+
Crema Finance (July 2022, ~$8.8M): the attacker supplied a forged tick account that the program trusted without adequate validation of its provenance, enabling fee data manipulation and flash-loan-funded drains.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Rule 003: Missing Discriminator Check (Type Cosplay)
|
|
2
|
+
|
|
3
|
+
**Severity:** High
|
|
4
|
+
**Category:** Account validation
|
|
5
|
+
|
|
6
|
+
## Description
|
|
7
|
+
Anchor prefixes every account with an 8-byte discriminator derived from the account type name, which distinguishes one account type from another at runtime. Code that deserializes account data manually — or uses types that skip the discriminator — allows an account of type A to be passed where type B is expected ("type cosplay"), as long as the byte layouts are compatible.
|
|
8
|
+
|
|
9
|
+
## Vulnerable pattern
|
|
10
|
+
```rust
|
|
11
|
+
#[derive(Accounts)]
|
|
12
|
+
pub struct AdminAction<'info> {
|
|
13
|
+
/// CHECK: admin config
|
|
14
|
+
pub admin_config: AccountInfo<'info>,
|
|
15
|
+
pub admin: Signer<'info>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub fn admin_action(ctx: Context<AdminAction>) -> Result<()> {
|
|
19
|
+
// UserAccount and AdminConfig are both { authority: Pubkey } —
|
|
20
|
+
// raw deserialization cannot tell them apart
|
|
21
|
+
let config = AdminConfig::try_from_slice(&ctx.accounts.admin_config.data.borrow())?;
|
|
22
|
+
require_keys_eq!(config.authority, ctx.accounts.admin.key());
|
|
23
|
+
Ok(())
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Why this is dangerous
|
|
28
|
+
An attacker initializes a low-privilege account type (e.g. their own `UserAccount`) whose layout matches the privileged type, then passes it where `AdminConfig` is expected. The deserialization succeeds with attacker-chosen field values, and the attacker passes authority checks meant for admins.
|
|
29
|
+
|
|
30
|
+
## Fix pattern
|
|
31
|
+
```rust
|
|
32
|
+
#[derive(Accounts)]
|
|
33
|
+
pub struct AdminAction<'info> {
|
|
34
|
+
// Account<'info, T> verifies the 8-byte discriminator and owner
|
|
35
|
+
pub admin_config: Account<'info, AdminConfig>,
|
|
36
|
+
pub admin: Signer<'info>,
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
In manual deserialization, compare the first 8 bytes against `AdminConfig::DISCRIMINATOR` before parsing.
|
|
40
|
+
|
|
41
|
+
## Detection heuristic
|
|
42
|
+
- Manual `try_from_slice` / borsh deserialization of full account data without slicing off and checking the first 8 bytes
|
|
43
|
+
- Account structs with identical or prefix-compatible field layouts used in the same program
|
|
44
|
+
- `#[account]` types deserialized through `AccountInfo` instead of `Account<'info, T>`
|
|
45
|
+
- Non-Anchor programs with multiple account types and no type/version tag byte
|
|
46
|
+
|
|
47
|
+
## References
|
|
48
|
+
- Coral sealevel-attacks — 3-type-cosplay (https://github.com/coral-xyz/sealevel-attacks/tree/master/programs/3-type-cosplay)
|
|
49
|
+
- Solana program security course — type cosplay (https://solana.com/developers/courses/program-security/type-cosplay)
|
|
50
|
+
- Helius — A Hitchhiker's Guide to Solana Program Security (https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security)
|
|
51
|
+
|
|
52
|
+
## Real-world exploits (if any)
|
|
53
|
+
No public headline exploit attributed solely to type cosplay; it is a recurring critical finding in public Sec3 and OtterSec audit reports of pre-launch programs.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Rule 004: Account Substitution (Missing Data Matching)
|
|
2
|
+
|
|
3
|
+
**Severity:** High
|
|
4
|
+
**Category:** Account validation
|
|
5
|
+
|
|
6
|
+
## Description
|
|
7
|
+
Two or more accounts in an instruction are logically related (a user and their state account, a vault and its token account) but no constraint enforces that relationship. Each account may individually be valid — correct owner, correct type — yet belong to a different user or pool than intended. The attacker substitutes a *valid but wrong* account.
|
|
8
|
+
|
|
9
|
+
## Vulnerable pattern
|
|
10
|
+
```rust
|
|
11
|
+
#[derive(Accounts)]
|
|
12
|
+
pub struct ClaimRewards<'info> {
|
|
13
|
+
pub user: Signer<'info>,
|
|
14
|
+
// Valid UserState account — but nothing ties it to `user`
|
|
15
|
+
#[account(mut)]
|
|
16
|
+
pub user_state: Account<'info, UserState>,
|
|
17
|
+
#[account(mut)]
|
|
18
|
+
pub reward_vault: Account<'info, TokenAccount>,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pub fn claim_rewards(ctx: Context<ClaimRewards>) -> Result<()> {
|
|
22
|
+
let amount = ctx.accounts.user_state.pending_rewards; // someone else's rewards
|
|
23
|
+
// ... transfer to user
|
|
24
|
+
Ok(())
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Why this is dangerous
|
|
29
|
+
The attacker signs with their own key but passes another user's `UserState` (or a state account from a different pool with a better exchange rate). All type and owner checks pass, and the attacker claims rewards, balances, or withdrawal rights that belong to someone else.
|
|
30
|
+
|
|
31
|
+
## Fix pattern
|
|
32
|
+
```rust
|
|
33
|
+
#[derive(Accounts)]
|
|
34
|
+
pub struct ClaimRewards<'info> {
|
|
35
|
+
pub user: Signer<'info>,
|
|
36
|
+
#[account(mut, has_one = user)] // UserState.user must equal user.key()
|
|
37
|
+
pub user_state: Account<'info, UserState>,
|
|
38
|
+
#[account(mut, address = user_state.reward_vault)]
|
|
39
|
+
pub reward_vault: Account<'info, TokenAccount>,
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Detection heuristic
|
|
44
|
+
- Multiple typed accounts in one context with no `has_one`, `constraint =`, `address =`, or shared PDA `seeds` linking them
|
|
45
|
+
- Handlers that index into one account using identity from another (user → user_state, pool → pool_vault) without an enforced link
|
|
46
|
+
- State structs that store related pubkeys (`user`, `mint`, `vault`) that are never compared against the passed accounts
|
|
47
|
+
|
|
48
|
+
## References
|
|
49
|
+
- Coral sealevel-attacks — 1-account-data-matching (https://github.com/coral-xyz/sealevel-attacks/tree/master/programs/1-account-data-matching)
|
|
50
|
+
- Solana program security course — account data matching (https://solana.com/developers/courses/program-security/account-data-matching)
|
|
51
|
+
- Neodyme — Solana common pitfalls (https://neodyme.io/en/blog/solana_common_pitfalls/)
|
|
52
|
+
|
|
53
|
+
## Real-world exploits (if any)
|
|
54
|
+
Cashio (March 2022, ~$48M) is the canonical case of an unvalidated account chain: a forged collateral chain passed individual checks but the links between accounts were never enforced end-to-end.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Rule 005: Sysvar Spoofing
|
|
2
|
+
|
|
3
|
+
**Severity:** High
|
|
4
|
+
**Category:** Account validation
|
|
5
|
+
|
|
6
|
+
## Description
|
|
7
|
+
Sysvar accounts (Clock, Rent, Instructions, EpochSchedule, …) live at well-known addresses, but a program that accepts "the clock account" or "the instructions sysvar" as an untyped `AccountInfo` and parses its data manually never verifies the address. An attacker passes a fake account with attacker-crafted "sysvar" contents — fake timestamps, fake serialized instructions — and the program trusts it.
|
|
8
|
+
|
|
9
|
+
## Vulnerable pattern
|
|
10
|
+
```rust
|
|
11
|
+
#[derive(Accounts)]
|
|
12
|
+
pub struct Verify<'info> {
|
|
13
|
+
/// CHECK: instructions sysvar
|
|
14
|
+
pub instructions: AccountInfo<'info>,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pub fn verify(ctx: Context<Verify>) -> Result<()> {
|
|
18
|
+
// Deprecated non-checked API: reads whatever account was passed
|
|
19
|
+
let ix = solana_program::sysvar::instructions::load_instruction_at(
|
|
20
|
+
0,
|
|
21
|
+
&ctx.accounts.instructions.data.borrow(),
|
|
22
|
+
)?;
|
|
23
|
+
// ... trusts `ix` to be a real instruction in this transaction
|
|
24
|
+
Ok(())
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Why this is dangerous
|
|
29
|
+
Whatever the program reads from the spoofed account — a timestamp gating a withdrawal, a "previous instruction" proving a signature verification ran — is attacker-controlled. In the worst case the attacker fabricates proof that a security check happened when it never did, bypassing the program's core authorization.
|
|
30
|
+
|
|
31
|
+
## Fix pattern
|
|
32
|
+
```rust
|
|
33
|
+
#[derive(Accounts)]
|
|
34
|
+
pub struct Verify<'info> {
|
|
35
|
+
/// CHECK: address constraint pins this to the real sysvar
|
|
36
|
+
#[account(address = solana_program::sysvar::instructions::ID)]
|
|
37
|
+
pub instructions: AccountInfo<'info>,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// In the handler, prefer the checked API:
|
|
41
|
+
let ix = solana_program::sysvar::instructions::load_instruction_at_checked(
|
|
42
|
+
0, &ctx.accounts.instructions)?;
|
|
43
|
+
```
|
|
44
|
+
For Clock/Rent, use `Sysvar<'info, Clock>` / `Clock::get()?` instead of passing accounts at all.
|
|
45
|
+
|
|
46
|
+
## Detection heuristic
|
|
47
|
+
- Sysvar-named accounts (`clock`, `rent`, `instructions`, `recent_blockhashes`) typed as `AccountInfo` without an `address =` constraint
|
|
48
|
+
- Use of deprecated `load_instruction_at` / `load_current_index` (non-`_checked` variants)
|
|
49
|
+
- Manual parsing of sysvar account data instead of `Clock::get()` / `Rent::get()`
|
|
50
|
+
|
|
51
|
+
## References
|
|
52
|
+
- Neodyme — Solana common pitfalls: solana_program::sysvar confusion (https://neodyme.io/en/blog/solana_common_pitfalls/)
|
|
53
|
+
- Helius — A Hitchhiker's Guide to Solana Program Security (https://www.helius.dev/blog/a-hitchhikers-guide-to-solana-program-security)
|
|
54
|
+
- Solana docs — sysvar cluster data (https://docs.solanalabs.com/runtime/sysvars)
|
|
55
|
+
|
|
56
|
+
## Real-world exploits (if any)
|
|
57
|
+
Wormhole bridge (February 2022, ~$325M): the guardian-signature verification used the deprecated non-checked instructions-sysvar API, letting the attacker substitute a fake sysvar account and forge a "signatures verified" result to mint 120k wETH.
|