brainblast 0.7.2 → 0.7.4
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/dist/{chunk-IY52XKWL.js → chunk-5H5JQXXU.js} +56 -1
- package/dist/{chunk-B2M3TZSA.js → chunk-5VYTURTO.js} +125 -1
- package/dist/{chunk-UWE6HAGS.js → chunk-F7QOW3IM.js} +1 -1
- package/dist/{chunk-SVSVVW6U.js → chunk-GF37ANSW.js} +46 -0
- package/dist/{chunk-2UZGWXIX.js → chunk-QMJEZ6NO.js} +14 -4
- package/dist/chunk-WQFLKWBY.js +110 -0
- package/dist/cli.js +95 -7
- package/dist/index.d.ts +87 -1
- package/dist/index.js +37 -5
- package/dist/{mcp-AM7MTCSZ.js → mcp-EOTWSQK7.js} +1 -1
- package/dist/oracle-DVZLFJ43.js +15 -0
- package/dist/rules/cpi-target-program-unverified.yaml +41 -0
- package/dist/{score-VLKER37D.js → score-YXFXD6QG.js} +2 -2
- package/dist/{trustGraph-4SSJOQKT.js → trustGraph-K5PWNEL4.js} +10 -2
- package/package.json +1 -1
|
@@ -996,6 +996,60 @@ function anchorBodyCallPattern(c, p) {
|
|
|
996
996
|
};
|
|
997
997
|
}
|
|
998
998
|
|
|
999
|
+
// src/checkers/anchorCpiUnverifiedProgram.ts
|
|
1000
|
+
var DEFAULT_PROGRAM_NAME = "(^|_)program$|^program(_id)?$";
|
|
1001
|
+
var DEFAULT_CPI = "invoke_signed\\s*\\(|invoke\\s*\\(|CpiContext::|\\.cpi\\(\\)|solana_program::program::invoke";
|
|
1002
|
+
var DEFAULT_VERIFIED_TYPE = "Program\\s*<";
|
|
1003
|
+
var DEFAULT_ADDRESS_CONSTRAINT = "address\\s*=";
|
|
1004
|
+
var RAW_TYPES = ["AccountInfo", "UncheckedAccount"];
|
|
1005
|
+
function isRawAccountType(typeName) {
|
|
1006
|
+
return RAW_TYPES.some((t) => typeName.includes(t));
|
|
1007
|
+
}
|
|
1008
|
+
function isProgramNamed(name, re) {
|
|
1009
|
+
return re.test(name);
|
|
1010
|
+
}
|
|
1011
|
+
function anchorCpiUnverifiedProgram(c, p) {
|
|
1012
|
+
const programNameRe = new RegExp(p?.programNameRegex ?? DEFAULT_PROGRAM_NAME, "i");
|
|
1013
|
+
const cpiRe = new RegExp(p?.cpiRegex ?? DEFAULT_CPI);
|
|
1014
|
+
const verifiedTypeRe = new RegExp(p?.verifiedTypeRegex ?? DEFAULT_VERIFIED_TYPE);
|
|
1015
|
+
const addressRe = new RegExp(p?.addressConstraintRegex ?? DEFAULT_ADDRESS_CONSTRAINT);
|
|
1016
|
+
const body = c.fnBodyText ?? "";
|
|
1017
|
+
const performsCpi = cpiRe.test(body);
|
|
1018
|
+
if (!performsCpi) {
|
|
1019
|
+
return {
|
|
1020
|
+
result: "cant_tell",
|
|
1021
|
+
detail: p?.absentDetail ?? `Handler '${c.fnName}' performs no cross-program invocation; CPI program-ID verification does not apply.`
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
const programFields = c.accountFields.filter((f) => isProgramNamed(f.name, programNameRe));
|
|
1025
|
+
if (programFields.length === 0) {
|
|
1026
|
+
return {
|
|
1027
|
+
result: "cant_tell",
|
|
1028
|
+
detail: p?.absentDetail ?? `Handler '${c.fnName}' performs a CPI but no program-named account field was found on '${c.accountStructName}' to verify.`
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
const isVerified = (f) => {
|
|
1032
|
+
if (verifiedTypeRe.test(f.typeName)) return true;
|
|
1033
|
+
if (!isRawAccountType(f.typeName)) return true;
|
|
1034
|
+
if (addressRe.test(f.attrText)) return true;
|
|
1035
|
+
const keyCheck = new RegExp(`${f.name}\\.key\\s*\\(\\s*\\)`);
|
|
1036
|
+
if (keyCheck.test(body)) return true;
|
|
1037
|
+
return false;
|
|
1038
|
+
};
|
|
1039
|
+
const unverified = programFields.filter((f) => !isVerified(f));
|
|
1040
|
+
if (unverified.length > 0) {
|
|
1041
|
+
const names = unverified.map((f) => `${f.name}: ${f.typeName}`).join(", ");
|
|
1042
|
+
return {
|
|
1043
|
+
result: "fail",
|
|
1044
|
+
detail: p?.failDetail ?? `Handler '${c.fnName}' performs a cross-program invocation but the target program account(s) [${names}] are raw AccountInfo/UncheckedAccount with no address verification. An attacker can substitute a malicious program \u2014 this is the Wormhole ($325M, Feb 2022) class of bug. Type the account as Program<'info, T> or add #[account(address = <expected_program_id>)].`
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
return {
|
|
1048
|
+
result: "pass",
|
|
1049
|
+
detail: p?.passDetail ?? `Handler '${c.fnName}' performs a CPI and every program account (${programFields.map((f) => f.name).join(", ")}) is identity-verified (Program<'info, _>, address= constraint, or in-body key check).`
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
999
1053
|
// src/checkers/index.ts
|
|
1000
1054
|
var registry = {
|
|
1001
1055
|
"positional-arg-identity": positionalArgIdentity,
|
|
@@ -1013,7 +1067,8 @@ var registry = {
|
|
|
1013
1067
|
"anchor-account-matches-idl": anchorIdlAccount,
|
|
1014
1068
|
"anchor-account-missing-constraint": anchorAccountMissingConstraint,
|
|
1015
1069
|
"anchor-forbidden-account-type": anchorForbiddenAccountType,
|
|
1016
|
-
"anchor-body-call-pattern": anchorBodyCallPattern
|
|
1070
|
+
"anchor-body-call-pattern": anchorBodyCallPattern,
|
|
1071
|
+
"anchor-cpi-unverified-program": anchorCpiUnverifiedProgram
|
|
1017
1072
|
};
|
|
1018
1073
|
function runChecker(kind, c, params) {
|
|
1019
1074
|
const fn = registry[kind];
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
loadPack,
|
|
8
8
|
resolveRules,
|
|
9
9
|
walk
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-5H5JQXXU.js";
|
|
11
11
|
|
|
12
12
|
// src/costAnalysis.ts
|
|
13
13
|
import { Project, SyntaxKind } from "ts-morph";
|
|
@@ -252,6 +252,123 @@ function renderCostReportMd(r) {
|
|
|
252
252
|
return lines.join("\n");
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
// src/exploitPatterns.ts
|
|
256
|
+
var EXPLOIT_PATTERNS = [
|
|
257
|
+
{
|
|
258
|
+
id: "wormhole",
|
|
259
|
+
title: "Wormhole bridge exploit",
|
|
260
|
+
date: "2022-02-02",
|
|
261
|
+
lossUsd: 325e6,
|
|
262
|
+
chain: "Solana",
|
|
263
|
+
postmortemUrl: "https://rekt.news/wormhole-rekt/",
|
|
264
|
+
rootCause: "A cross-program invocation / signature-verification load trusted an account by position, not by identity. A deprecated load did not verify the supplied sysvar's address, so an attacker substituted their own account and forged a 'verified' signature set.",
|
|
265
|
+
ruleId: "cpi-target-program-unverified",
|
|
266
|
+
detects: "An Anchor handler that performs a CPI (invoke / invoke_signed / CpiContext) against a program-named account typed as raw AccountInfo/UncheckedAccount with no address= constraint and no in-body key check.",
|
|
267
|
+
fix: "Type the program account as Program<'info, T> (Anchor verifies the program id on deserialization) or add #[account(address = <expected_program_id>)]."
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: "cashio",
|
|
271
|
+
title: "Cashio infinite-mint exploit",
|
|
272
|
+
date: "2022-03-23",
|
|
273
|
+
lossUsd: 48e6,
|
|
274
|
+
chain: "Solana",
|
|
275
|
+
postmortemUrl: "https://rekt.news/cashio-rekt/",
|
|
276
|
+
rootCause: "A missing account-validation chain: the program accepted attacker-supplied accounts without verifying their owner/identity, letting a fake collateral account mint unbacked CASH.",
|
|
277
|
+
ruleId: "anchor-unchecked-account-type",
|
|
278
|
+
detects: "UncheckedAccount<'info> fields in instruction handlers \u2014 accounts Anchor performs zero runtime validation on (owner, signer, data layout all unchecked).",
|
|
279
|
+
fix: "Replace UncheckedAccount with Account<'info, T>, Signer<'info>, or SystemAccount<'info> so Anchor enforces ownership and type before the handler runs."
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: "crema",
|
|
283
|
+
title: "Crema Finance exploit",
|
|
284
|
+
date: "2022-07-03",
|
|
285
|
+
lossUsd: 88e5,
|
|
286
|
+
chain: "Solana",
|
|
287
|
+
postmortemUrl: "https://rekt.news/crema-finance-rekt/",
|
|
288
|
+
rootCause: "A forged 'tick' account was passed into the program and accepted without identity validation, letting the attacker report fake fee data and drain liquidity.",
|
|
289
|
+
ruleId: "anchor-unchecked-account-type",
|
|
290
|
+
detects: "The same unchecked-account class: a security-critical account taken as UncheckedAccount with no ownership/type enforcement.",
|
|
291
|
+
fix: "Deserialize the account through a typed Account<'info, T> (or add an explicit owner/address constraint) so a forged account is rejected."
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
id: "fake-mint-impersonation",
|
|
295
|
+
title: "SPL mint impersonation (counterfeit tokens)",
|
|
296
|
+
chain: "Solana",
|
|
297
|
+
postmortemUrl: "https://solana.com/developers/guides/token-extensions/metadata-pointer",
|
|
298
|
+
rootCause: "A counterfeit SPL mint reuses a legitimate token's name/symbol/metadata; integrations that key off symbol or metadata instead of the canonical mint address route value to the wrong token.",
|
|
299
|
+
ruleId: "solana-token-impersonation",
|
|
300
|
+
detects: "Code that resolves or trusts a token by symbol/name rather than pinning the canonical mint address, against the bundled canonical-mint registry.",
|
|
301
|
+
fix: "Pin and compare the canonical mint address (Pubkey) for every supported token; never trust symbol/name/metadata as identity."
|
|
302
|
+
}
|
|
303
|
+
];
|
|
304
|
+
function getExploitPattern(idOrRule) {
|
|
305
|
+
return EXPLOIT_PATTERNS.find((e) => e.id === idOrRule || e.ruleId === idOrRule);
|
|
306
|
+
}
|
|
307
|
+
function formatUsd(n) {
|
|
308
|
+
if (n == null) return "\u2014";
|
|
309
|
+
if (n >= 1e9) return `$${(n / 1e9).toFixed(2)}B`;
|
|
310
|
+
if (n >= 1e6) return `$${(n / 1e6).toFixed(1)}M`;
|
|
311
|
+
if (n >= 1e3) return `$${(n / 1e3).toFixed(0)}K`;
|
|
312
|
+
return `$${n}`;
|
|
313
|
+
}
|
|
314
|
+
function totalLossUsd(patterns = EXPLOIT_PATTERNS) {
|
|
315
|
+
return patterns.reduce((s, e) => s + (e.lossUsd ?? 0), 0);
|
|
316
|
+
}
|
|
317
|
+
function renderExploitsMd(patterns = EXPLOIT_PATTERNS) {
|
|
318
|
+
const L = ["## Exploit Pattern Database\n"];
|
|
319
|
+
L.push(
|
|
320
|
+
"Real on-chain incidents, each mapped to the bundled brainblast rule that statically detects its root-cause pattern. The code that lost these funds is the exact code these rules fail on.\n"
|
|
321
|
+
);
|
|
322
|
+
L.push("| Incident | Date | Loss | Detecting rule |");
|
|
323
|
+
L.push("|----------|------|------|----------------|");
|
|
324
|
+
for (const e of patterns) {
|
|
325
|
+
L.push(
|
|
326
|
+
`| [${e.title}](${e.postmortemUrl}) | ${e.date ?? "ongoing"} | ${formatUsd(e.lossUsd)} | \`${e.ruleId}\` |`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
const total = totalLossUsd(patterns);
|
|
330
|
+
L.push("");
|
|
331
|
+
L.push(`**Catalogued losses: ${formatUsd(total)} across ${patterns.length} patterns.**
|
|
332
|
+
`);
|
|
333
|
+
for (const e of patterns) {
|
|
334
|
+
L.push(`### ${e.title}${e.lossUsd ? ` \u2014 ${formatUsd(e.lossUsd)}` : ""}
|
|
335
|
+
`);
|
|
336
|
+
L.push(`- **Chain / date:** ${e.chain} \xB7 ${e.date ?? "ongoing class"}`);
|
|
337
|
+
L.push(`- **Post-mortem:** ${e.postmortemUrl}`);
|
|
338
|
+
L.push(`- **Root cause:** ${e.rootCause}`);
|
|
339
|
+
L.push(`- **Detected by:** \`${e.ruleId}\` \u2014 ${e.detects}`);
|
|
340
|
+
L.push(`- **Fix:** ${e.fix}`);
|
|
341
|
+
L.push("");
|
|
342
|
+
}
|
|
343
|
+
return L.join("\n");
|
|
344
|
+
}
|
|
345
|
+
function renderExploitsText(patterns = EXPLOIT_PATTERNS) {
|
|
346
|
+
const L = [];
|
|
347
|
+
L.push("\u2500\u2500 Exploit Pattern Database \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
348
|
+
L.push(" real incidents \u2192 the bundled rule that detects each root cause");
|
|
349
|
+
L.push("");
|
|
350
|
+
for (const e of patterns) {
|
|
351
|
+
L.push(` ${e.title} (${e.date ?? "ongoing"}, ${formatUsd(e.lossUsd)}, ${e.chain})`);
|
|
352
|
+
L.push(` rule: ${e.ruleId}`);
|
|
353
|
+
L.push(` cause: ${e.rootCause}`);
|
|
354
|
+
L.push("");
|
|
355
|
+
}
|
|
356
|
+
L.push(` catalogued losses: ${formatUsd(totalLossUsd(patterns))} across ${patterns.length} patterns`);
|
|
357
|
+
return L.join("\n");
|
|
358
|
+
}
|
|
359
|
+
function renderExploitDetailText(e) {
|
|
360
|
+
const L = [];
|
|
361
|
+
L.push(`\u2500\u2500 ${e.title} \u2500\u2500`);
|
|
362
|
+
L.push(` chain/date: ${e.chain} \xB7 ${e.date ?? "ongoing class"}`);
|
|
363
|
+
L.push(` loss: ${formatUsd(e.lossUsd)}`);
|
|
364
|
+
L.push(` postmortem: ${e.postmortemUrl}`);
|
|
365
|
+
L.push(` root cause: ${e.rootCause}`);
|
|
366
|
+
L.push(` detected by: ${e.ruleId}`);
|
|
367
|
+
L.push(` ${e.detects}`);
|
|
368
|
+
L.push(` fix: ${e.fix}`);
|
|
369
|
+
return L.join("\n");
|
|
370
|
+
}
|
|
371
|
+
|
|
255
372
|
// src/watch.ts
|
|
256
373
|
import { watch as fsWatch } from "fs";
|
|
257
374
|
function runIncrementalScan(targetDir, rules, emit) {
|
|
@@ -492,6 +609,13 @@ export {
|
|
|
492
609
|
lamportsToSol,
|
|
493
610
|
analyzeCosts,
|
|
494
611
|
renderCostReportMd,
|
|
612
|
+
EXPLOIT_PATTERNS,
|
|
613
|
+
getExploitPattern,
|
|
614
|
+
formatUsd,
|
|
615
|
+
totalLossUsd,
|
|
616
|
+
renderExploitsMd,
|
|
617
|
+
renderExploitsText,
|
|
618
|
+
renderExploitDetailText,
|
|
495
619
|
runIncrementalScan,
|
|
496
620
|
startWatch,
|
|
497
621
|
parseDiff,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
getAccountInfo,
|
|
2
3
|
probeUpgradeAuthority
|
|
3
4
|
} from "./chunk-XSVQSK53.js";
|
|
4
5
|
|
|
@@ -39,6 +40,41 @@ function loadDirectory(path = bundledPath()) {
|
|
|
39
40
|
return m;
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
// src/trustGraph/classifyAuthority.ts
|
|
44
|
+
var SYSTEM_PROGRAM = "11111111111111111111111111111111";
|
|
45
|
+
var KNOWN_AUTHORITY_OWNERS = {
|
|
46
|
+
// Squads — the dominant Solana multisig. v3 ("SMPL") and v4 program ids.
|
|
47
|
+
// https://docs.squads.so/main/development/sdk/program-ids
|
|
48
|
+
SMPLecH534NA9acpos4G6x7uf3LWbCAwZQE9e8ZekMu: { kind: "multisig", label: "Squads v3" },
|
|
49
|
+
SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf: { kind: "multisig", label: "Squads v4" },
|
|
50
|
+
// SPL Governance (Realms) — the standard on-chain governance program.
|
|
51
|
+
// https://github.com/solana-labs/solana-program-library/tree/master/governance
|
|
52
|
+
GovER5Lthms3bLBqWub97yVrMmEogzX7xNjdXpPPCVZw: { kind: "dao", label: "SPL Governance (Realms)" }
|
|
53
|
+
};
|
|
54
|
+
async function classifyUpgradeAuthority(address, opts = {}) {
|
|
55
|
+
const acct = await getAccountInfo(address, opts);
|
|
56
|
+
if (!acct) {
|
|
57
|
+
return { kind: "unknown" };
|
|
58
|
+
}
|
|
59
|
+
if (acct.owner === SYSTEM_PROGRAM) {
|
|
60
|
+
return { kind: "single-key", ownerProgram: SYSTEM_PROGRAM };
|
|
61
|
+
}
|
|
62
|
+
const known = KNOWN_AUTHORITY_OWNERS[acct.owner];
|
|
63
|
+
if (known) {
|
|
64
|
+
return { kind: known.kind, ownerProgram: acct.owner, ownerLabel: known.label };
|
|
65
|
+
}
|
|
66
|
+
return { kind: "unknown", ownerProgram: acct.owner };
|
|
67
|
+
}
|
|
68
|
+
async function enrichAuthorityClassification(authority, opts = {}) {
|
|
69
|
+
if (authority.kind !== "unknown" || !authority.address) return authority;
|
|
70
|
+
const c = await classifyUpgradeAuthority(authority.address, opts);
|
|
71
|
+
return {
|
|
72
|
+
...authority,
|
|
73
|
+
kind: c.kind,
|
|
74
|
+
...c.ownerProgram ? { ownerProgram: c.ownerProgram } : {}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
42
78
|
// src/trustGraph/programCache.ts
|
|
43
79
|
import { readFileSync as readFileSync2, writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
|
|
44
80
|
import { join as join2, dirname } from "path";
|
|
@@ -146,6 +182,12 @@ async function buildTrustGraph(programIds, opts = {}) {
|
|
|
146
182
|
let authority;
|
|
147
183
|
try {
|
|
148
184
|
authority = await probeUpgradeAuthority(id, opts);
|
|
185
|
+
if (opts.classifyAuthority !== false) {
|
|
186
|
+
try {
|
|
187
|
+
authority = await enrichAuthorityClassification(authority, opts);
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
}
|
|
149
191
|
} catch (e) {
|
|
150
192
|
unresolved.push({ programId: id, reason: `rpc_error: ${e?.message ?? String(e)}` });
|
|
151
193
|
continue;
|
|
@@ -174,6 +216,10 @@ async function buildTrustGraph(programIds, opts = {}) {
|
|
|
174
216
|
|
|
175
217
|
export {
|
|
176
218
|
loadDirectory,
|
|
219
|
+
SYSTEM_PROGRAM,
|
|
220
|
+
KNOWN_AUTHORITY_OWNERS,
|
|
221
|
+
classifyUpgradeAuthority,
|
|
222
|
+
enrichAuthorityClassification,
|
|
177
223
|
DEFAULT_TTL_HOURS,
|
|
178
224
|
defaultCachePath,
|
|
179
225
|
loadProgramCache,
|
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
// src/trustGraph/render.ts
|
|
2
2
|
function renderAuthority(p) {
|
|
3
3
|
const a = p.upgradeAuthority;
|
|
4
|
+
const owner = a.ownerProgram ? ` _(owner: \`${a.ownerProgram}\`)_` : "";
|
|
4
5
|
switch (a.kind) {
|
|
5
6
|
case "renounced":
|
|
6
7
|
return "\u{1F512} **Renounced** \u2014 program is frozen; no key can upgrade it.";
|
|
7
8
|
case "single-key":
|
|
8
|
-
return `\u26A0\uFE0F **Single key** \`${a.address}\` \u2014 one private key can replace this program at any time
|
|
9
|
+
return `\u26A0\uFE0F **Single key** \`${a.address}\` \u2014 one private key can replace this program at any time.${owner}`;
|
|
9
10
|
case "multisig":
|
|
10
|
-
return `\u{1F510} **Multisig** \`${a.address}\` \u2014 a threshold of signers can upgrade
|
|
11
|
+
return `\u{1F510} **Multisig** \`${a.address}\` \u2014 a threshold of signers can upgrade.${owner}`;
|
|
11
12
|
case "dao":
|
|
12
|
-
return `\u{1F3DB} **DAO** \`${a.address}\` \u2014 governance program controls upgrades
|
|
13
|
+
return `\u{1F3DB} **DAO** \`${a.address}\` \u2014 governance program controls upgrades.${owner}`;
|
|
13
14
|
case "unknown":
|
|
14
|
-
return a.address ? `\u2753 **Unclassified authority** \`${a.address}
|
|
15
|
+
return a.address ? `\u2753 **Unclassified authority** \`${a.address}\`${owner} \u2014 needs research to confirm single-key vs multisig/DAO.` : "\u2753 **Unknown** \u2014 could not determine upgrade authority.";
|
|
15
16
|
}
|
|
16
17
|
}
|
|
18
|
+
function renderTrustSummary(p) {
|
|
19
|
+
const a = p.upgradeAuthority;
|
|
20
|
+
const authBit = a.kind === "renounced" ? "\u{1F512} immutable" : a.kind === "multisig" ? "\u{1F510} multisig" : a.kind === "dao" ? "\u{1F3DB} DAO-governed" : a.kind === "single-key" ? "\u26A0\uFE0F single-key upgradeable" : "\u2753 authority unclassified";
|
|
21
|
+
const verifiedBit = p.verifiedBuild.state === "verified" ? "\u2705 verified build" : p.verifiedBuild.state === "unverified" ? "\u274C unverified" : "\u2753 build unchecked";
|
|
22
|
+
const auditBit = p.audits.length ? `\u2705 audited (${p.audits.map((x) => x.firm).join(", ")})` : "\u274C no audits on file";
|
|
23
|
+
return `${authBit} \xB7 ${verifiedBit} \xB7 ${auditBit}`;
|
|
24
|
+
}
|
|
17
25
|
function renderVerified(p) {
|
|
18
26
|
const v = p.verifiedBuild;
|
|
19
27
|
switch (v.state) {
|
|
@@ -42,6 +50,8 @@ function renderProgram(p) {
|
|
|
42
50
|
"",
|
|
43
51
|
`\`${p.programId}\`${p.kind ? ` \xB7 kind: \`${p.kind}\`` : ""}`,
|
|
44
52
|
"",
|
|
53
|
+
`**Trust:** ${renderTrustSummary(p)}`,
|
|
54
|
+
"",
|
|
45
55
|
`- **Upgrade authority:** ${renderAuthority(p)}`,
|
|
46
56
|
`- **Verified build:** ${renderVerified(p)}`,
|
|
47
57
|
`- **Parity:** ${renderParity(p)}`,
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_RPC
|
|
3
|
+
} from "./chunk-XSVQSK53.js";
|
|
4
|
+
import {
|
|
5
|
+
isValidSolanaAddress
|
|
6
|
+
} from "./chunk-VG5FMOLW.js";
|
|
7
|
+
|
|
8
|
+
// src/oracle.ts
|
|
9
|
+
var SLOT_MS = 400;
|
|
10
|
+
var DEFAULT_STALENESS_SLOTS = 150;
|
|
11
|
+
async function rpc(method, params, opts) {
|
|
12
|
+
const url = opts.rpcUrl ?? DEFAULT_RPC;
|
|
13
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
14
|
+
const ac = new AbortController();
|
|
15
|
+
const t = setTimeout(() => ac.abort(), opts.timeoutMs ?? 1e4);
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetchImpl(url, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: { "content-type": "application/json" },
|
|
20
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
|
|
21
|
+
signal: ac.signal
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok) throw new Error(`rpc ${method}: HTTP ${res.status}`);
|
|
24
|
+
const body = await res.json();
|
|
25
|
+
if (body.error) throw new Error(`rpc ${method}: ${body.error.message}`);
|
|
26
|
+
if (body.result === void 0) throw new Error(`rpc ${method}: empty result`);
|
|
27
|
+
return body.result;
|
|
28
|
+
} finally {
|
|
29
|
+
clearTimeout(t);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function checkOracleFreshness(account, opts = {}) {
|
|
33
|
+
if (!isValidSolanaAddress(account)) throw new Error(`invalid Solana address: ${account}`);
|
|
34
|
+
const thresholdSlots = opts.maxStalenessSlots ?? (opts.maxStalenessSeconds != null ? Math.max(1, Math.round(opts.maxStalenessSeconds * 1e3 / SLOT_MS)) : DEFAULT_STALENESS_SLOTS);
|
|
35
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
36
|
+
const currentSlot = await rpc("getSlot", [{ commitment: "confirmed" }], opts);
|
|
37
|
+
const sigs = await rpc(
|
|
38
|
+
"getSignaturesForAddress",
|
|
39
|
+
[account, { limit: 1 }],
|
|
40
|
+
opts
|
|
41
|
+
);
|
|
42
|
+
if (!sigs || sigs.length === 0) {
|
|
43
|
+
return {
|
|
44
|
+
account,
|
|
45
|
+
currentSlot,
|
|
46
|
+
lastSlot: null,
|
|
47
|
+
lastBlockTime: null,
|
|
48
|
+
slotsBehind: null,
|
|
49
|
+
secondsBehind: null,
|
|
50
|
+
thresholdSlots,
|
|
51
|
+
fresh: false,
|
|
52
|
+
verdict: "NO_HISTORY",
|
|
53
|
+
checkedAt
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const last = sigs[0];
|
|
57
|
+
const lastSlot = last.slot;
|
|
58
|
+
const lastBlockTime = last.blockTime ?? null;
|
|
59
|
+
const slotsBehind = Math.max(0, currentSlot - lastSlot);
|
|
60
|
+
const secondsBehind = lastBlockTime != null ? Math.max(0, Math.floor(Date.now() / 1e3) - lastBlockTime) : Math.round(slotsBehind * SLOT_MS / 1e3);
|
|
61
|
+
const fresh = slotsBehind <= thresholdSlots;
|
|
62
|
+
return {
|
|
63
|
+
account,
|
|
64
|
+
currentSlot,
|
|
65
|
+
lastSlot,
|
|
66
|
+
lastBlockTime,
|
|
67
|
+
slotsBehind,
|
|
68
|
+
secondsBehind,
|
|
69
|
+
thresholdSlots,
|
|
70
|
+
fresh,
|
|
71
|
+
verdict: fresh ? "FRESH" : "STALE",
|
|
72
|
+
checkedAt
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function renderOracleText(f) {
|
|
76
|
+
const L = [];
|
|
77
|
+
L.push("\u2500\u2500 Oracle Freshness \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
78
|
+
L.push(` account: ${f.account}`);
|
|
79
|
+
L.push(` current slot: ${f.currentSlot.toLocaleString()}`);
|
|
80
|
+
if (f.verdict === "NO_HISTORY") {
|
|
81
|
+
L.push(" last write: (no transactions found touching this account)");
|
|
82
|
+
L.push(" verdict: \u2753 NO_HISTORY \u2014 cannot confirm freshness");
|
|
83
|
+
return L.join("\n");
|
|
84
|
+
}
|
|
85
|
+
L.push(` last write: slot ${f.lastSlot.toLocaleString()} (${f.slotsBehind.toLocaleString()} slots / ~${f.secondsBehind}s ago)`);
|
|
86
|
+
L.push(` threshold: ${f.thresholdSlots.toLocaleString()} slots`);
|
|
87
|
+
L.push(` verdict: ${f.fresh ? "\u2705 FRESH" : "\u{1F6A8} STALE \u2014 last update is older than the freshness threshold"}`);
|
|
88
|
+
return L.join("\n");
|
|
89
|
+
}
|
|
90
|
+
function renderOracleMd(f) {
|
|
91
|
+
const L = ["## Oracle Freshness\n"];
|
|
92
|
+
L.push(`**Account:** \`${f.account}\` \xB7 checked ${f.checkedAt}
|
|
93
|
+
`);
|
|
94
|
+
if (f.verdict === "NO_HISTORY") {
|
|
95
|
+
L.push("\u2753 **NO_HISTORY** \u2014 no transactions were found touching this account, so freshness can't be confirmed. Double-check the address.");
|
|
96
|
+
return L.join("\n");
|
|
97
|
+
}
|
|
98
|
+
const badge = f.fresh ? "\u2705 **FRESH**" : "\u{1F6A8} **STALE**";
|
|
99
|
+
L.push(`${badge} \u2014 last written at slot ${f.lastSlot.toLocaleString()}, ${f.slotsBehind.toLocaleString()} slots (~${f.secondsBehind}s) behind the current slot (${f.currentSlot.toLocaleString()}).`);
|
|
100
|
+
L.push("");
|
|
101
|
+
L.push(`Threshold: ${f.thresholdSlots.toLocaleString()} slots. ${f.fresh ? "Within tolerance." : "**Exceeds tolerance \u2014 do not price against this feed until it updates.**"}`);
|
|
102
|
+
return L.join("\n");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
DEFAULT_STALENESS_SLOTS,
|
|
107
|
+
checkOracleFreshness,
|
|
108
|
+
renderOracleText,
|
|
109
|
+
renderOracleMd
|
|
110
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -1,33 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
EXPLOIT_PATTERNS,
|
|
3
4
|
analyzeCosts,
|
|
4
5
|
applyDiffToFile,
|
|
6
|
+
getExploitPattern,
|
|
5
7
|
initPack,
|
|
6
8
|
isTelemetryEnabled,
|
|
7
9
|
lamportsToSol,
|
|
8
10
|
parseDiff,
|
|
9
11
|
recordGraduationEvents,
|
|
10
12
|
renderCostReportMd,
|
|
13
|
+
renderExploitDetailText,
|
|
14
|
+
renderExploitsMd,
|
|
15
|
+
renderExploitsText,
|
|
11
16
|
rentExemptMinimum,
|
|
12
17
|
startWatch,
|
|
13
18
|
submitTelemetry,
|
|
14
19
|
telemetryFilePath,
|
|
15
20
|
validatePack
|
|
16
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-5VYTURTO.js";
|
|
17
22
|
import {
|
|
18
23
|
renderTrustGraphMd
|
|
19
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-QMJEZ6NO.js";
|
|
20
25
|
import {
|
|
21
26
|
buildTrustGraph,
|
|
22
27
|
cacheSize,
|
|
23
28
|
defaultCachePath,
|
|
24
29
|
loadProgramCache
|
|
25
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-GF37ANSW.js";
|
|
26
31
|
import {
|
|
27
32
|
audit,
|
|
28
33
|
getChangedRanges,
|
|
29
34
|
resolveRules
|
|
30
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-5H5JQXXU.js";
|
|
31
36
|
import "./chunk-2XJORJPQ.js";
|
|
32
37
|
import "./chunk-O5Z4ZJHC.js";
|
|
33
38
|
import "./chunk-XSVQSK53.js";
|
|
@@ -475,7 +480,7 @@ if (args[0] === "drift") {
|
|
|
475
480
|
process.exit(0);
|
|
476
481
|
}
|
|
477
482
|
if (args[0] === "mcp") {
|
|
478
|
-
const { startMcpServer } = await import("./mcp-
|
|
483
|
+
const { startMcpServer } = await import("./mcp-EOTWSQK7.js");
|
|
479
484
|
await startMcpServer();
|
|
480
485
|
process.exit(0);
|
|
481
486
|
}
|
|
@@ -554,6 +559,14 @@ if (args[0] === "deploy-plan") {
|
|
|
554
559
|
runDeployPlan(args.slice(1));
|
|
555
560
|
process.exit(0);
|
|
556
561
|
}
|
|
562
|
+
if (args[0] === "exploits") {
|
|
563
|
+
runExploits(args.slice(1));
|
|
564
|
+
process.exit(0);
|
|
565
|
+
}
|
|
566
|
+
if (args[0] === "oracle") {
|
|
567
|
+
await runOracle(args.slice(1));
|
|
568
|
+
process.exit(0);
|
|
569
|
+
}
|
|
557
570
|
if (args[0] === "fix") {
|
|
558
571
|
await runFix(args.slice(1));
|
|
559
572
|
process.exit(0);
|
|
@@ -691,6 +704,81 @@ function runDeployPlan(argv) {
|
|
|
691
704
|
writeFileSync2(mdPath, renderDeployPlanMd(plan));
|
|
692
705
|
console.log(` deploy plan: ${mdPath}`);
|
|
693
706
|
}
|
|
707
|
+
async function runOracle(argv) {
|
|
708
|
+
if (argv.includes("--help") || argv.includes("-h") || argv.filter((a) => !a.startsWith("--")).length === 0) {
|
|
709
|
+
console.log("usage: brainblast oracle <account> [--rpc URL] [--max-staleness-slots N | --max-staleness-seconds N] [--json]");
|
|
710
|
+
console.log(" Is the oracle fresh? Reports how many slots/seconds ago the price account was");
|
|
711
|
+
console.log(" last written (provider-agnostic) and gates FRESH/STALE. Exit 1 on STALE or");
|
|
712
|
+
console.log(" NO_HISTORY. Pass your own --rpc for reliable results (public RPC is rate-limited).");
|
|
713
|
+
process.exit(argv.length === 0 ? 2 : 0);
|
|
714
|
+
}
|
|
715
|
+
const { checkOracleFreshness, renderOracleText, renderOracleMd } = await import("./oracle-DVZLFJ43.js");
|
|
716
|
+
const num = (name) => {
|
|
717
|
+
const i = argv.indexOf(`--${name}`);
|
|
718
|
+
if (i < 0) return void 0;
|
|
719
|
+
const v = parseInt(argv[i + 1], 10);
|
|
720
|
+
return Number.isFinite(v) ? v : void 0;
|
|
721
|
+
};
|
|
722
|
+
const rpcIdx = argv.indexOf("--rpc");
|
|
723
|
+
const rpcUrl = rpcIdx >= 0 ? argv[rpcIdx + 1] : void 0;
|
|
724
|
+
const account = argv.find(
|
|
725
|
+
(a, i) => !a.startsWith("--") && argv[i - 1] !== "--rpc" && argv[i - 1] !== "--max-staleness-slots" && argv[i - 1] !== "--max-staleness-seconds"
|
|
726
|
+
);
|
|
727
|
+
if (!account) {
|
|
728
|
+
console.error("error: missing <account>. usage: brainblast oracle <account> [--rpc URL] [--json]");
|
|
729
|
+
process.exit(2);
|
|
730
|
+
}
|
|
731
|
+
let f;
|
|
732
|
+
try {
|
|
733
|
+
f = await checkOracleFreshness(account, {
|
|
734
|
+
rpcUrl,
|
|
735
|
+
maxStalenessSlots: num("max-staleness-slots"),
|
|
736
|
+
maxStalenessSeconds: num("max-staleness-seconds")
|
|
737
|
+
});
|
|
738
|
+
} catch (e) {
|
|
739
|
+
console.error(`error: ${e?.message ?? String(e)}`);
|
|
740
|
+
process.exit(2);
|
|
741
|
+
}
|
|
742
|
+
if (argv.includes("--json")) console.log(JSON.stringify(f, null, 2));
|
|
743
|
+
else console.log(renderOracleText(f));
|
|
744
|
+
const outDir2 = join3(process.cwd(), ".agent-research");
|
|
745
|
+
mkdirSync2(outDir2, { recursive: true });
|
|
746
|
+
writeFileSync2(join3(outDir2, "oracle-freshness.md"), renderOracleMd(f));
|
|
747
|
+
process.exit(f.fresh ? 0 : 1);
|
|
748
|
+
}
|
|
749
|
+
function runExploits(argv) {
|
|
750
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
751
|
+
console.log("usage: brainblast exploits [id] [--json]");
|
|
752
|
+
console.log(" The Exploit Pattern Database: real on-chain incidents mapped to the bundled");
|
|
753
|
+
console.log(" rule that statically detects each root-cause pattern. Pass an incident id or");
|
|
754
|
+
console.log(" rule id to see one in detail. Known ids:");
|
|
755
|
+
console.log(` ${EXPLOIT_PATTERNS.map((e) => e.id).join(", ")}`);
|
|
756
|
+
process.exit(0);
|
|
757
|
+
}
|
|
758
|
+
const json = argv.includes("--json");
|
|
759
|
+
const id = argv.find((a) => !a.startsWith("--"));
|
|
760
|
+
if (id) {
|
|
761
|
+
const e = getExploitPattern(id);
|
|
762
|
+
if (!e) {
|
|
763
|
+
console.error(`error: no exploit pattern '${id}'. Known: ${EXPLOIT_PATTERNS.map((x) => x.id).join(", ")}`);
|
|
764
|
+
process.exit(2);
|
|
765
|
+
}
|
|
766
|
+
if (json) console.log(JSON.stringify(e, null, 2));
|
|
767
|
+
else console.log(renderExploitDetailText(e));
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (json) {
|
|
771
|
+
console.log(JSON.stringify(EXPLOIT_PATTERNS, null, 2));
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
console.log(renderExploitsText());
|
|
775
|
+
const outDir2 = join3(process.cwd(), ".agent-research");
|
|
776
|
+
mkdirSync2(outDir2, { recursive: true });
|
|
777
|
+
const mdPath = join3(outDir2, "exploit-patterns.md");
|
|
778
|
+
writeFileSync2(mdPath, renderExploitsMd());
|
|
779
|
+
console.log(`
|
|
780
|
+
database: ${mdPath}`);
|
|
781
|
+
}
|
|
694
782
|
function runPack(argv) {
|
|
695
783
|
const sub = argv[0];
|
|
696
784
|
if (sub === "init") {
|
|
@@ -1130,8 +1218,8 @@ async function runIdlRules(argv) {
|
|
|
1130
1218
|
}
|
|
1131
1219
|
}
|
|
1132
1220
|
async function runScore(argv) {
|
|
1133
|
-
const { scoreProgram, renderScoreText, gradeAtLeast } = await import("./score-
|
|
1134
|
-
const { isValidSolanaAddress: isValidSolanaAddress2 } = await import("./trustGraph-
|
|
1221
|
+
const { scoreProgram, renderScoreText, gradeAtLeast } = await import("./score-YXFXD6QG.js");
|
|
1222
|
+
const { isValidSolanaAddress: isValidSolanaAddress2 } = await import("./trustGraph-K5PWNEL4.js");
|
|
1135
1223
|
const programId = argv.find((a) => !a.startsWith("--"));
|
|
1136
1224
|
if (!programId) {
|
|
1137
1225
|
console.error("usage: brainblast score <program-id> [--rpc URL] [--no-probe] [--min A|B|C|D|F] [--json]");
|
package/dist/index.d.ts
CHANGED
|
@@ -156,6 +156,21 @@ interface Rule {
|
|
|
156
156
|
kind: string;
|
|
157
157
|
params?: Record<string, any>;
|
|
158
158
|
};
|
|
159
|
+
/**
|
|
160
|
+
* Exploit Pattern Database provenance (v0.7.3): links this rule to the real
|
|
161
|
+
* on-chain incident whose root cause it detects. Optional; present on rules
|
|
162
|
+
* derived from a public post-mortem. The authoritative, user-facing catalog
|
|
163
|
+
* lives in src/exploitPatterns.ts — this inline block is rule-local
|
|
164
|
+
* provenance and is cross-checked against the catalog in tests.
|
|
165
|
+
*/
|
|
166
|
+
exploit?: {
|
|
167
|
+
incident: string;
|
|
168
|
+
date: string;
|
|
169
|
+
lossUsd?: number;
|
|
170
|
+
chain?: string;
|
|
171
|
+
postmortemUrl: string;
|
|
172
|
+
pattern?: string;
|
|
173
|
+
};
|
|
159
174
|
/**
|
|
160
175
|
* Provenance for rules loaded from a third-party rule pack (see
|
|
161
176
|
* src/packs.ts). Absent for bundled rules. Stamped by the pack loader from
|
|
@@ -373,6 +388,7 @@ interface UpgradeAuthority {
|
|
|
373
388
|
address: string | null;
|
|
374
389
|
source: UpgradeAuthoritySource;
|
|
375
390
|
checkedAt?: string;
|
|
391
|
+
ownerProgram?: string;
|
|
376
392
|
}
|
|
377
393
|
type VerifiedBuildState = {
|
|
378
394
|
state: "verified";
|
|
@@ -428,6 +444,7 @@ interface RpcOpts {
|
|
|
428
444
|
|
|
429
445
|
interface BuildOpts extends RpcOpts {
|
|
430
446
|
probeRpc?: boolean;
|
|
447
|
+
classifyAuthority?: boolean;
|
|
431
448
|
directoryPath?: string;
|
|
432
449
|
cachePath?: string | null;
|
|
433
450
|
}
|
|
@@ -437,6 +454,20 @@ declare function renderTrustGraphMd(g: TrustGraph): string;
|
|
|
437
454
|
|
|
438
455
|
declare function loadDirectory(path?: string): Map<string, OnChainProgram>;
|
|
439
456
|
|
|
457
|
+
declare const SYSTEM_PROGRAM = "11111111111111111111111111111111";
|
|
458
|
+
interface KnownOwner {
|
|
459
|
+
kind: Exclude<UpgradeAuthorityKind, "renounced">;
|
|
460
|
+
label: string;
|
|
461
|
+
}
|
|
462
|
+
declare const KNOWN_AUTHORITY_OWNERS: Record<string, KnownOwner>;
|
|
463
|
+
interface AuthorityClassification {
|
|
464
|
+
kind: UpgradeAuthorityKind;
|
|
465
|
+
ownerProgram?: string;
|
|
466
|
+
ownerLabel?: string;
|
|
467
|
+
}
|
|
468
|
+
declare function classifyUpgradeAuthority(address: string, opts?: RpcOpts): Promise<AuthorityClassification>;
|
|
469
|
+
declare function enrichAuthorityClassification(authority: UpgradeAuthority, opts?: RpcOpts): Promise<UpgradeAuthority>;
|
|
470
|
+
|
|
440
471
|
declare function base58Encode(bytes: Uint8Array): string;
|
|
441
472
|
declare function base58Decode(s: string): Uint8Array;
|
|
442
473
|
declare function isValidSolanaAddress(s: string): boolean;
|
|
@@ -861,4 +892,59 @@ declare function batchScan(mints: string[], opts?: BatchScanOpts): Promise<Batch
|
|
|
861
892
|
declare function parseMintList(content: string): string[];
|
|
862
893
|
declare function renderBatchText(result: BatchResult): string;
|
|
863
894
|
|
|
864
|
-
|
|
895
|
+
declare const DEFAULT_STALENESS_SLOTS = 150;
|
|
896
|
+
type OracleVerdict = "FRESH" | "STALE" | "NO_HISTORY";
|
|
897
|
+
interface OracleFreshness {
|
|
898
|
+
account: string;
|
|
899
|
+
currentSlot: number;
|
|
900
|
+
/** Slot of the most recent signature touching the account; null if none. */
|
|
901
|
+
lastSlot: number | null;
|
|
902
|
+
/** Unix seconds of that signature's block, when the RPC provides it. */
|
|
903
|
+
lastBlockTime: number | null;
|
|
904
|
+
slotsBehind: number | null;
|
|
905
|
+
secondsBehind: number | null;
|
|
906
|
+
thresholdSlots: number;
|
|
907
|
+
fresh: boolean;
|
|
908
|
+
verdict: OracleVerdict;
|
|
909
|
+
checkedAt: string;
|
|
910
|
+
}
|
|
911
|
+
interface OracleOpts extends RpcOpts {
|
|
912
|
+
/** Staleness threshold in slots (default 150 ≈ 60s). */
|
|
913
|
+
maxStalenessSlots?: number;
|
|
914
|
+
/** Staleness threshold in seconds; converted to slots (~400ms each). */
|
|
915
|
+
maxStalenessSeconds?: number;
|
|
916
|
+
}
|
|
917
|
+
declare function checkOracleFreshness(account: string, opts?: OracleOpts): Promise<OracleFreshness>;
|
|
918
|
+
declare function renderOracleText(f: OracleFreshness): string;
|
|
919
|
+
declare function renderOracleMd(f: OracleFreshness): string;
|
|
920
|
+
|
|
921
|
+
interface ExploitPattern {
|
|
922
|
+
/** Stable slug, e.g. "wormhole". */
|
|
923
|
+
id: string;
|
|
924
|
+
/** Human title of the incident. */
|
|
925
|
+
title: string;
|
|
926
|
+
/** ISO date of the exploit (YYYY-MM-DD), or undefined for a pattern class. */
|
|
927
|
+
date?: string;
|
|
928
|
+
/** Reported loss in USD, when a single figure is meaningful. */
|
|
929
|
+
lossUsd?: number;
|
|
930
|
+
chain: string;
|
|
931
|
+
/** Public post-mortem / incident analysis URL. */
|
|
932
|
+
postmortemUrl: string;
|
|
933
|
+
/** The generalized root cause, in one sentence. */
|
|
934
|
+
rootCause: string;
|
|
935
|
+
/** Bundled rule id that detects this pattern (must exist in rules/). */
|
|
936
|
+
ruleId: string;
|
|
937
|
+
/** What that rule looks for, in plain English. */
|
|
938
|
+
detects: string;
|
|
939
|
+
/** The safe pattern that closes the hole. */
|
|
940
|
+
fix: string;
|
|
941
|
+
}
|
|
942
|
+
declare const EXPLOIT_PATTERNS: ExploitPattern[];
|
|
943
|
+
declare function getExploitPattern(idOrRule: string): ExploitPattern | undefined;
|
|
944
|
+
declare function formatUsd(n: number | undefined): string;
|
|
945
|
+
declare function totalLossUsd(patterns?: ExploitPattern[]): number;
|
|
946
|
+
declare function renderExploitsMd(patterns?: ExploitPattern[]): string;
|
|
947
|
+
declare function renderExploitsText(patterns?: ExploitPattern[]): string;
|
|
948
|
+
declare function renderExploitDetailText(e: ExploitPattern): string;
|
|
949
|
+
|
|
950
|
+
export { type AccountFlow, type AnchorIdl, type AuditRef, type AuthorityClassification, type BatchResult, type BatchRow, type BatchScanOpts, type BuildOpts, CANONICAL_BY_MINT, CANONICAL_MINTS, type Candidate, type CanonicalMint, type ChainEvent, type ChainWatchOpts, type ChainWatchState, type ChangedRanges, type CheckOutcome, type CheckResult, type CheckResultKind, type Checker, type ConfigCandidate, type ConfigChecker, type CostReport, DEFAULT_REGISTRY_URL, DEFAULT_STALENESS_SLOTS, DEFAULT_TTL_HOURS, type DecodedInstruction, type DecodedTx, type DiffResult, type DriftAdvisory, type DriftBaseline, type DriftPackage, type DriftResult, EXPLOIT_PATTERNS, type ExploitPattern, type FirewallFinding, type FirewallOpts, type FirewallProgram, type FirewallReport, type FirewallSeverity, type FirewallVerdict, type Grade, type GraduationEvent, type IdentityStatus, type IdlAccount, type IdlConstraintParams, type IdlInstruction, KNOWN_AUTHORITY_OWNERS, KNOWN_PROGRAMS, type MintInfo, type OnChainProgram, type OracleFreshness, type OracleOpts, type OracleVerdict, type OsvAdvisory, PACK_MANIFEST_FILE, type PackInitOptions, type PackManifest, type PackRuleValidation, type PackValidateResult, type ParityNote, type ParsedDiff, type PreflightCheck, type PreflightOpts, type PreflightReport, type PreflightStatus, type PreflightVerdict, type PriorityFeePosture, type ProgramCache, type ProgramCacheEntry, type Recoverability, type RicoOutcome, type RicoResult, type RicoTokenSecurity, type Rule, type RustAccountField, type RustCandidate, type RustChecker, SYSTEM_PROGRAM, type ScoreFactor, type Severity, type TelemetrySubmitResult, type TokenIdentity, type TrustGraph, type TrustScore, type UpgradeAuthority, type UpgradeAuthorityKind, type UpgradeAuthoritySource, type VerifiedBuildState, type VerifyOpts, type WatchEvent, type WatchOptions, analyzeCosts, analyzeInstructions, analyzeToken, applyDiffToFile, audit, auditWithRule, base58Decode, base58Encode, batchScan, buildConstraintParams, buildTrustGraph, rules as bundledRules, cacheSize, canonicalMintForSymbol, checkDrift, checkOracleFreshness, checkerKinds, classifyUpgradeAuthority, decodeTransaction, defaultCachePath, deployerFlagsFrom, diffVersions, enrichAuthorityClassification, fileChanged, findCandidates, findConfigCandidates, formatUsd, generateRulesFromIdl, generateTestForResult, getCacheEntry, getCacheEntryMeta, getChangedRanges, getExploitPattern, getRepoHash, getUserHash, getWorkingTreeChanges, gradeAtLeast, gradeForScore, idlProgramName, initPack, initialChainWatchState, inspectTransaction, isCanonicalMint, isEntryExpired, isTelemetryEnabled, isValidSolanaAddress, lamportsToSol, loadDirectory, loadPack, loadPacksFromDir, loadProgramCache, loadRules, parseCpiPrograms, parseDiff, parseIdl, parseMintAccount, parseMintList, pollChainOnce, pumpPreflight, putCacheEntry, queryOsv, rangeChanged, recordGraduationEvents, renderBatchText, renderCostReportMd, renderDiffMd, renderDiffText, renderDriftText, renderExploitDetailText, renderExploitsMd, renderExploitsText, renderFirewallText, renderOracleMd, renderOracleText, renderPreflightText, renderRicoText, renderRulesYaml, renderScoreText, renderTest, renderTrustGraphMd, rentExemptMinimum, resolveRules, riskScore, runChecker, runIncrementalScan, saveProgramCache, scoreFromProgram, scoreProgram, seedPackages, startChainWatch, startWatch, submitTelemetry, telemetryFilePath, testKinds, toSnakeCase, totalLossUsd, validatePack, validatePackManifest, verifyTokenIdentity };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_STALENESS_SLOTS,
|
|
3
|
+
checkOracleFreshness,
|
|
4
|
+
renderOracleMd,
|
|
5
|
+
renderOracleText
|
|
6
|
+
} from "./chunk-WQFLKWBY.js";
|
|
1
7
|
import {
|
|
2
8
|
checkDrift,
|
|
3
9
|
renderDriftText,
|
|
@@ -17,7 +23,7 @@ import {
|
|
|
17
23
|
renderScoreText,
|
|
18
24
|
scoreFromProgram,
|
|
19
25
|
scoreProgram
|
|
20
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-F7QOW3IM.js";
|
|
21
27
|
import {
|
|
22
28
|
parseMintAccount,
|
|
23
29
|
pumpPreflight,
|
|
@@ -38,8 +44,11 @@ import {
|
|
|
38
44
|
} from "./chunk-VI2JBH2T.js";
|
|
39
45
|
import {
|
|
40
46
|
DEFAULT_REGISTRY_URL,
|
|
47
|
+
EXPLOIT_PATTERNS,
|
|
41
48
|
analyzeCosts,
|
|
42
49
|
applyDiffToFile,
|
|
50
|
+
formatUsd,
|
|
51
|
+
getExploitPattern,
|
|
43
52
|
getRepoHash,
|
|
44
53
|
getUserHash,
|
|
45
54
|
initPack,
|
|
@@ -48,21 +57,29 @@ import {
|
|
|
48
57
|
parseDiff,
|
|
49
58
|
recordGraduationEvents,
|
|
50
59
|
renderCostReportMd,
|
|
60
|
+
renderExploitDetailText,
|
|
61
|
+
renderExploitsMd,
|
|
62
|
+
renderExploitsText,
|
|
51
63
|
rentExemptMinimum,
|
|
52
64
|
runIncrementalScan,
|
|
53
65
|
startWatch,
|
|
54
66
|
submitTelemetry,
|
|
55
67
|
telemetryFilePath,
|
|
68
|
+
totalLossUsd,
|
|
56
69
|
validatePack
|
|
57
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-5VYTURTO.js";
|
|
58
71
|
import {
|
|
59
72
|
renderTrustGraphMd
|
|
60
|
-
} from "./chunk-
|
|
73
|
+
} from "./chunk-QMJEZ6NO.js";
|
|
61
74
|
import {
|
|
62
75
|
DEFAULT_TTL_HOURS,
|
|
76
|
+
KNOWN_AUTHORITY_OWNERS,
|
|
77
|
+
SYSTEM_PROGRAM,
|
|
63
78
|
buildTrustGraph,
|
|
64
79
|
cacheSize,
|
|
80
|
+
classifyUpgradeAuthority,
|
|
65
81
|
defaultCachePath,
|
|
82
|
+
enrichAuthorityClassification,
|
|
66
83
|
getCacheEntry,
|
|
67
84
|
getCacheEntryMeta,
|
|
68
85
|
isEntryExpired,
|
|
@@ -70,7 +87,7 @@ import {
|
|
|
70
87
|
loadProgramCache,
|
|
71
88
|
putCacheEntry,
|
|
72
89
|
saveProgramCache
|
|
73
|
-
} from "./chunk-
|
|
90
|
+
} from "./chunk-GF37ANSW.js";
|
|
74
91
|
import {
|
|
75
92
|
PACK_MANIFEST_FILE,
|
|
76
93
|
audit,
|
|
@@ -91,7 +108,7 @@ import {
|
|
|
91
108
|
runChecker,
|
|
92
109
|
testKinds,
|
|
93
110
|
validatePackManifest
|
|
94
|
-
} from "./chunk-
|
|
111
|
+
} from "./chunk-5H5JQXXU.js";
|
|
95
112
|
import {
|
|
96
113
|
CANONICAL_BY_MINT,
|
|
97
114
|
CANONICAL_MINTS,
|
|
@@ -143,9 +160,13 @@ export {
|
|
|
143
160
|
CANONICAL_BY_MINT,
|
|
144
161
|
CANONICAL_MINTS,
|
|
145
162
|
DEFAULT_REGISTRY_URL,
|
|
163
|
+
DEFAULT_STALENESS_SLOTS,
|
|
146
164
|
DEFAULT_TTL_HOURS,
|
|
165
|
+
EXPLOIT_PATTERNS,
|
|
166
|
+
KNOWN_AUTHORITY_OWNERS,
|
|
147
167
|
KNOWN_PROGRAMS,
|
|
148
168
|
PACK_MANIFEST_FILE,
|
|
169
|
+
SYSTEM_PROGRAM,
|
|
149
170
|
analyzeCosts,
|
|
150
171
|
analyzeInstructions,
|
|
151
172
|
analyzeToken,
|
|
@@ -161,19 +182,24 @@ export {
|
|
|
161
182
|
cacheSize,
|
|
162
183
|
canonicalMintForSymbol,
|
|
163
184
|
checkDrift,
|
|
185
|
+
checkOracleFreshness,
|
|
164
186
|
checkerKinds,
|
|
187
|
+
classifyUpgradeAuthority,
|
|
165
188
|
decodeTransaction,
|
|
166
189
|
defaultCachePath,
|
|
167
190
|
deployerFlagsFrom,
|
|
168
191
|
diffVersions,
|
|
192
|
+
enrichAuthorityClassification,
|
|
169
193
|
fileChanged,
|
|
170
194
|
findCandidates,
|
|
171
195
|
findConfigCandidates,
|
|
196
|
+
formatUsd,
|
|
172
197
|
generateRulesFromIdl,
|
|
173
198
|
generateTestForResult,
|
|
174
199
|
getCacheEntry,
|
|
175
200
|
getCacheEntryMeta,
|
|
176
201
|
getChangedRanges,
|
|
202
|
+
getExploitPattern,
|
|
177
203
|
getRepoHash,
|
|
178
204
|
getUserHash,
|
|
179
205
|
getWorkingTreeChanges,
|
|
@@ -209,7 +235,12 @@ export {
|
|
|
209
235
|
renderDiffMd,
|
|
210
236
|
renderDiffText,
|
|
211
237
|
renderDriftText,
|
|
238
|
+
renderExploitDetailText,
|
|
239
|
+
renderExploitsMd,
|
|
240
|
+
renderExploitsText,
|
|
212
241
|
renderFirewallText,
|
|
242
|
+
renderOracleMd,
|
|
243
|
+
renderOracleText,
|
|
213
244
|
renderPreflightText,
|
|
214
245
|
renderRicoText,
|
|
215
246
|
renderRulesYaml,
|
|
@@ -231,6 +262,7 @@ export {
|
|
|
231
262
|
telemetryFilePath,
|
|
232
263
|
testKinds,
|
|
233
264
|
toSnakeCase,
|
|
265
|
+
totalLossUsd,
|
|
234
266
|
validatePack,
|
|
235
267
|
validatePackManifest,
|
|
236
268
|
verifyTokenIdentity
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_STALENESS_SLOTS,
|
|
3
|
+
checkOracleFreshness,
|
|
4
|
+
renderOracleMd,
|
|
5
|
+
renderOracleText
|
|
6
|
+
} from "./chunk-WQFLKWBY.js";
|
|
7
|
+
import "./chunk-XSVQSK53.js";
|
|
8
|
+
import "./chunk-VG5FMOLW.js";
|
|
9
|
+
import "./chunk-3RG5ZIWI.js";
|
|
10
|
+
export {
|
|
11
|
+
DEFAULT_STALENESS_SLOTS,
|
|
12
|
+
checkOracleFreshness,
|
|
13
|
+
renderOracleMd,
|
|
14
|
+
renderOracleText
|
|
15
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
id: cpi-target-program-unverified
|
|
2
|
+
severity: critical
|
|
3
|
+
title: Anchor CPI invokes a target program account whose identity is never verified (Wormhole pattern)
|
|
4
|
+
component:
|
|
5
|
+
name: Anchor Framework
|
|
6
|
+
type: Blockchain
|
|
7
|
+
version: ">=0.26.0"
|
|
8
|
+
sourceUrl: https://www.anchor-lang.com/docs/the-program-module
|
|
9
|
+
exploit:
|
|
10
|
+
incident: Wormhole bridge exploit
|
|
11
|
+
date: "2022-02-02"
|
|
12
|
+
lossUsd: 325000000
|
|
13
|
+
chain: Solana
|
|
14
|
+
postmortemUrl: https://rekt.news/wormhole-rekt/
|
|
15
|
+
pattern: >-
|
|
16
|
+
A cross-program invocation (or signature-verification load) was performed
|
|
17
|
+
against an account trusted by position rather than by identity. A deprecated
|
|
18
|
+
load did not verify the supplied account's address, letting an attacker
|
|
19
|
+
substitute a malicious account and forge verification. Generalized: a CPI
|
|
20
|
+
target program was used without verifying its program ID.
|
|
21
|
+
detect:
|
|
22
|
+
lang: rust
|
|
23
|
+
modules:
|
|
24
|
+
- "@coral-xyz/anchor"
|
|
25
|
+
- "@project-serum/anchor"
|
|
26
|
+
nameRegex: ".*"
|
|
27
|
+
triggerCalls: []
|
|
28
|
+
check:
|
|
29
|
+
kind: anchor-cpi-unverified-program
|
|
30
|
+
params:
|
|
31
|
+
programNameRegex: "(^|_)program$|^program(_id)?$"
|
|
32
|
+
cpiRegex: "invoke_signed\\s*\\(|invoke\\s*\\(|CpiContext::|\\.cpi\\(\\)"
|
|
33
|
+
test:
|
|
34
|
+
kind: anchor-program-test
|
|
35
|
+
params:
|
|
36
|
+
scenario: cpi-substituted-program
|
|
37
|
+
description: >-
|
|
38
|
+
Invokes the handler with a malicious program substituted for the expected
|
|
39
|
+
target. With a raw AccountInfo program account the CPI dispatches to the
|
|
40
|
+
attacker's program; typing it Program<'info, T> (or adding address=)
|
|
41
|
+
makes Anchor reject the substitution before the handler runs.
|
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
renderScoreText,
|
|
5
5
|
scoreFromProgram,
|
|
6
6
|
scoreProgram
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-F7QOW3IM.js";
|
|
8
|
+
import "./chunk-GF37ANSW.js";
|
|
9
9
|
import "./chunk-XSVQSK53.js";
|
|
10
10
|
import "./chunk-VG5FMOLW.js";
|
|
11
11
|
import "./chunk-3RG5ZIWI.js";
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
renderProgram,
|
|
3
3
|
renderTrustGraphMd
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-QMJEZ6NO.js";
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_TTL_HOURS,
|
|
7
|
+
KNOWN_AUTHORITY_OWNERS,
|
|
8
|
+
SYSTEM_PROGRAM,
|
|
7
9
|
buildTrustGraph,
|
|
8
10
|
cacheSize,
|
|
11
|
+
classifyUpgradeAuthority,
|
|
9
12
|
defaultCachePath,
|
|
13
|
+
enrichAuthorityClassification,
|
|
10
14
|
getCacheEntry,
|
|
11
15
|
getCacheEntryMeta,
|
|
12
16
|
isEntryExpired,
|
|
@@ -14,7 +18,7 @@ import {
|
|
|
14
18
|
loadProgramCache,
|
|
15
19
|
putCacheEntry,
|
|
16
20
|
saveProgramCache
|
|
17
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-GF37ANSW.js";
|
|
18
22
|
import {
|
|
19
23
|
DEFAULT_RPC,
|
|
20
24
|
getAccountInfo,
|
|
@@ -29,11 +33,15 @@ import "./chunk-3RG5ZIWI.js";
|
|
|
29
33
|
export {
|
|
30
34
|
DEFAULT_RPC,
|
|
31
35
|
DEFAULT_TTL_HOURS,
|
|
36
|
+
KNOWN_AUTHORITY_OWNERS,
|
|
37
|
+
SYSTEM_PROGRAM,
|
|
32
38
|
base58Decode,
|
|
33
39
|
base58Encode,
|
|
34
40
|
buildTrustGraph,
|
|
35
41
|
cacheSize,
|
|
42
|
+
classifyUpgradeAuthority,
|
|
36
43
|
defaultCachePath,
|
|
44
|
+
enrichAuthorityClassification,
|
|
37
45
|
getAccountInfo,
|
|
38
46
|
getCacheEntry,
|
|
39
47
|
getCacheEntryMeta,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainblast",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic auditor for catastrophic AI-integration bugs: scan a repo, find the silent money/auth traps, and generate the behavioral test that proves they're fixed.",
|
|
6
6
|
"keywords": [
|