brainblast 0.7.2 → 0.7.3

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.
@@ -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-IY52XKWL.js";
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,
package/dist/cli.js CHANGED
@@ -1,19 +1,24 @@
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-B2M3TZSA.js";
21
+ } from "./chunk-5VYTURTO.js";
17
22
  import {
18
23
  renderTrustGraphMd
19
24
  } from "./chunk-2UZGWXIX.js";
@@ -27,7 +32,7 @@ import {
27
32
  audit,
28
33
  getChangedRanges,
29
34
  resolveRules
30
- } from "./chunk-IY52XKWL.js";
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-AM7MTCSZ.js");
483
+ const { startMcpServer } = await import("./mcp-EOTWSQK7.js");
479
484
  await startMcpServer();
480
485
  process.exit(0);
481
486
  }
@@ -554,6 +559,10 @@ 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
+ }
557
566
  if (args[0] === "fix") {
558
567
  await runFix(args.slice(1));
559
568
  process.exit(0);
@@ -691,6 +700,39 @@ function runDeployPlan(argv) {
691
700
  writeFileSync2(mdPath, renderDeployPlanMd(plan));
692
701
  console.log(` deploy plan: ${mdPath}`);
693
702
  }
703
+ function runExploits(argv) {
704
+ if (argv.includes("--help") || argv.includes("-h")) {
705
+ console.log("usage: brainblast exploits [id] [--json]");
706
+ console.log(" The Exploit Pattern Database: real on-chain incidents mapped to the bundled");
707
+ console.log(" rule that statically detects each root-cause pattern. Pass an incident id or");
708
+ console.log(" rule id to see one in detail. Known ids:");
709
+ console.log(` ${EXPLOIT_PATTERNS.map((e) => e.id).join(", ")}`);
710
+ process.exit(0);
711
+ }
712
+ const json = argv.includes("--json");
713
+ const id = argv.find((a) => !a.startsWith("--"));
714
+ if (id) {
715
+ const e = getExploitPattern(id);
716
+ if (!e) {
717
+ console.error(`error: no exploit pattern '${id}'. Known: ${EXPLOIT_PATTERNS.map((x) => x.id).join(", ")}`);
718
+ process.exit(2);
719
+ }
720
+ if (json) console.log(JSON.stringify(e, null, 2));
721
+ else console.log(renderExploitDetailText(e));
722
+ return;
723
+ }
724
+ if (json) {
725
+ console.log(JSON.stringify(EXPLOIT_PATTERNS, null, 2));
726
+ return;
727
+ }
728
+ console.log(renderExploitsText());
729
+ const outDir2 = join3(process.cwd(), ".agent-research");
730
+ mkdirSync2(outDir2, { recursive: true });
731
+ const mdPath = join3(outDir2, "exploit-patterns.md");
732
+ writeFileSync2(mdPath, renderExploitsMd());
733
+ console.log(`
734
+ database: ${mdPath}`);
735
+ }
694
736
  function runPack(argv) {
695
737
  const sub = argv[0];
696
738
  if (sub === "init") {
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
@@ -861,4 +876,33 @@ declare function batchScan(mints: string[], opts?: BatchScanOpts): Promise<Batch
861
876
  declare function parseMintList(content: string): string[];
862
877
  declare function renderBatchText(result: BatchResult): string;
863
878
 
864
- export { type AccountFlow, type AnchorIdl, type AuditRef, 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_TTL_HOURS, type DecodedInstruction, type DecodedTx, type DiffResult, type DriftAdvisory, type DriftBaseline, type DriftPackage, type DriftResult, 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_PROGRAMS, type MintInfo, type OnChainProgram, 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, 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, checkerKinds, decodeTransaction, defaultCachePath, deployerFlagsFrom, diffVersions, fileChanged, findCandidates, findConfigCandidates, generateRulesFromIdl, generateTestForResult, getCacheEntry, getCacheEntryMeta, getChangedRanges, 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, renderFirewallText, renderPreflightText, renderRicoText, renderRulesYaml, renderScoreText, renderTest, renderTrustGraphMd, rentExemptMinimum, resolveRules, riskScore, runChecker, runIncrementalScan, saveProgramCache, scoreFromProgram, scoreProgram, seedPackages, startChainWatch, startWatch, submitTelemetry, telemetryFilePath, testKinds, toSnakeCase, validatePack, validatePackManifest, verifyTokenIdentity };
879
+ interface ExploitPattern {
880
+ /** Stable slug, e.g. "wormhole". */
881
+ id: string;
882
+ /** Human title of the incident. */
883
+ title: string;
884
+ /** ISO date of the exploit (YYYY-MM-DD), or undefined for a pattern class. */
885
+ date?: string;
886
+ /** Reported loss in USD, when a single figure is meaningful. */
887
+ lossUsd?: number;
888
+ chain: string;
889
+ /** Public post-mortem / incident analysis URL. */
890
+ postmortemUrl: string;
891
+ /** The generalized root cause, in one sentence. */
892
+ rootCause: string;
893
+ /** Bundled rule id that detects this pattern (must exist in rules/). */
894
+ ruleId: string;
895
+ /** What that rule looks for, in plain English. */
896
+ detects: string;
897
+ /** The safe pattern that closes the hole. */
898
+ fix: string;
899
+ }
900
+ declare const EXPLOIT_PATTERNS: ExploitPattern[];
901
+ declare function getExploitPattern(idOrRule: string): ExploitPattern | undefined;
902
+ declare function formatUsd(n: number | undefined): string;
903
+ declare function totalLossUsd(patterns?: ExploitPattern[]): number;
904
+ declare function renderExploitsMd(patterns?: ExploitPattern[]): string;
905
+ declare function renderExploitsText(patterns?: ExploitPattern[]): string;
906
+ declare function renderExploitDetailText(e: ExploitPattern): string;
907
+
908
+ export { type AccountFlow, type AnchorIdl, type AuditRef, 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_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_PROGRAMS, type MintInfo, type OnChainProgram, 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, 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, checkerKinds, decodeTransaction, defaultCachePath, deployerFlagsFrom, diffVersions, 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, 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
@@ -38,8 +38,11 @@ import {
38
38
  } from "./chunk-VI2JBH2T.js";
39
39
  import {
40
40
  DEFAULT_REGISTRY_URL,
41
+ EXPLOIT_PATTERNS,
41
42
  analyzeCosts,
42
43
  applyDiffToFile,
44
+ formatUsd,
45
+ getExploitPattern,
43
46
  getRepoHash,
44
47
  getUserHash,
45
48
  initPack,
@@ -48,13 +51,17 @@ import {
48
51
  parseDiff,
49
52
  recordGraduationEvents,
50
53
  renderCostReportMd,
54
+ renderExploitDetailText,
55
+ renderExploitsMd,
56
+ renderExploitsText,
51
57
  rentExemptMinimum,
52
58
  runIncrementalScan,
53
59
  startWatch,
54
60
  submitTelemetry,
55
61
  telemetryFilePath,
62
+ totalLossUsd,
56
63
  validatePack
57
- } from "./chunk-B2M3TZSA.js";
64
+ } from "./chunk-5VYTURTO.js";
58
65
  import {
59
66
  renderTrustGraphMd
60
67
  } from "./chunk-2UZGWXIX.js";
@@ -91,7 +98,7 @@ import {
91
98
  runChecker,
92
99
  testKinds,
93
100
  validatePackManifest
94
- } from "./chunk-IY52XKWL.js";
101
+ } from "./chunk-5H5JQXXU.js";
95
102
  import {
96
103
  CANONICAL_BY_MINT,
97
104
  CANONICAL_MINTS,
@@ -144,6 +151,7 @@ export {
144
151
  CANONICAL_MINTS,
145
152
  DEFAULT_REGISTRY_URL,
146
153
  DEFAULT_TTL_HOURS,
154
+ EXPLOIT_PATTERNS,
147
155
  KNOWN_PROGRAMS,
148
156
  PACK_MANIFEST_FILE,
149
157
  analyzeCosts,
@@ -169,11 +177,13 @@ export {
169
177
  fileChanged,
170
178
  findCandidates,
171
179
  findConfigCandidates,
180
+ formatUsd,
172
181
  generateRulesFromIdl,
173
182
  generateTestForResult,
174
183
  getCacheEntry,
175
184
  getCacheEntryMeta,
176
185
  getChangedRanges,
186
+ getExploitPattern,
177
187
  getRepoHash,
178
188
  getUserHash,
179
189
  getWorkingTreeChanges,
@@ -209,6 +219,9 @@ export {
209
219
  renderDiffMd,
210
220
  renderDiffText,
211
221
  renderDriftText,
222
+ renderExploitDetailText,
223
+ renderExploitsMd,
224
+ renderExploitsText,
212
225
  renderFirewallText,
213
226
  renderPreflightText,
214
227
  renderRicoText,
@@ -231,6 +244,7 @@ export {
231
244
  telemetryFilePath,
232
245
  testKinds,
233
246
  toSnakeCase,
247
+ totalLossUsd,
234
248
  validatePack,
235
249
  validatePackManifest,
236
250
  verifyTokenIdentity
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  audit,
3
3
  resolveRules
4
- } from "./chunk-IY52XKWL.js";
4
+ } from "./chunk-5H5JQXXU.js";
5
5
  import "./chunk-2XJORJPQ.js";
6
6
  import "./chunk-O5Z4ZJHC.js";
7
7
  import {
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainblast",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
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": [