@elytrasec/engine 0.4.5 → 0.4.6

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.
Files changed (2) hide show
  1. package/dist/index.js +89 -1
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2912,6 +2912,7 @@ var PY = [".py"];
2912
2912
  var GO = [".go"];
2913
2913
  var SOL = [".sol"];
2914
2914
  var JAVA = [".java"];
2915
+ var RUST = [".rs"];
2915
2916
  var RUBY = [".rb", ".erb"];
2916
2917
  var PHP = [".php"];
2917
2918
  var ALL = ["*"];
@@ -5229,6 +5230,86 @@ var modernDeFiRules = [
5229
5230
  languages: SOL
5230
5231
  }
5231
5232
  ];
5233
+ var solanaRules = [
5234
+ {
5235
+ id: "cp-sl-missing-signer-check",
5236
+ title: "Solana: account used as authority without signer check",
5237
+ description: "An account is passed to an instruction and used as the authority for an action (transfer, mint, close) without verifying it has signed the transaction. The function deserializes the account but never checks `is_signer` or uses the Anchor `Signer<'info>` type.",
5238
+ suggestion: "Anchor: declare the authority as `pub authority: Signer<'info>` so the runtime enforces is_signer. Raw: `require!(ctx.accounts.authority.is_signer, ErrorCode::Unauthorized);`",
5239
+ multilinePattern: /pub\s+\w+\s*:\s*AccountInfo\s*<'info>[\s\S]{0,400}?(?:transfer|mint_to|close|set_authority|burn)\s*\((?![\s\S]{0,800}?(?:is_signer|Signer\s*<|require!\s*\([^)]*is_signer|require_keys_eq!\s*\([^)]*authority))/,
5240
+ severity: "critical",
5241
+ category: "solidity",
5242
+ confidence: "medium",
5243
+ languages: RUST
5244
+ },
5245
+ {
5246
+ id: "cp-sl-missing-owner-check",
5247
+ title: "Solana: account ownership not verified before deserialization",
5248
+ description: "Raw `AccountInfo` is deserialized with `try_from_slice` or accessed via `data.borrow()` without first checking `account.owner == &expected_program_id`. An attacker can pass any account holding crafted bytes, leading to type confusion.",
5249
+ suggestion: "Anchor: use typed account wrappers (`Account<'info, MyState>`) which check the discriminator automatically. Raw: `require_keys_eq!(*account.owner, program_id, ErrorCode::WrongOwner);` before any deserialization.",
5250
+ multilinePattern: /(?:\.|::)try_from_slice\s*\(\s*(?:&|&mut\s+)\s*\w+\.(?:data|try_borrow_data)(?![\s\S]{0,800}?(?:require_keys_eq!\s*\([^)]*\.owner|require!\s*\([^)]*\.owner\s*==|assert_eq!\s*\([^)]*\.owner|Account\s*<|AccountLoader\s*<))/,
5251
+ severity: "critical",
5252
+ category: "solidity",
5253
+ confidence: "medium",
5254
+ languages: RUST
5255
+ },
5256
+ {
5257
+ id: "cp-sl-ata-confusion",
5258
+ title: "Solana: token account mint not validated (ATA confusion)",
5259
+ description: "An SPL `TokenAccount` is used in a transfer without validating its `mint` field matches the expected mint. An attacker can substitute a token account they control with the same authority but different (worthless) mint, draining the real account.",
5260
+ suggestion: "Anchor: use `#[account(mint = expected_mint, token::authority = user)]` constraints. Raw: `require_keys_eq!(token_account.mint, expected_mint);`",
5261
+ multilinePattern: /token::transfer\s*\([\s\S]{0,400}?\)(?![\s\S]{0,1500}?(?:mint\s*=\s*\w+|mint\s*==\s*\w+|require_keys_eq!\s*\([^)]*\.mint|associated_token::))/,
5262
+ severity: "high",
5263
+ category: "solidity",
5264
+ confidence: "low",
5265
+ languages: RUST
5266
+ },
5267
+ {
5268
+ id: "cp-sl-account-substitution",
5269
+ title: "Solana: account passed without has_one or constraint binding",
5270
+ description: "An account is destructured from `ctx.accounts` and read/written, but the program never checks that this account is the one expected by the user's PDA-derived state. An attacker can substitute any same-typed account.",
5271
+ suggestion: "Use Anchor's `#[account(has_one = expected_field)]` or `#[account(constraint = a.key() == state.b)]` to bind related accounts. Raw: `require_keys_eq!(ctx.accounts.target.key(), state.target);`",
5272
+ multilinePattern: /#\[derive\s*\(\s*Accounts\s*\)\][\s\S]{0,2000}?pub\s+\w+\s*:\s*Account\s*<\s*'info\s*,\s*\w+\s*>\s*,(?![\s\S]{0,2000}?(?:has_one\s*=|constraint\s*=|seeds\s*=))/,
5273
+ // Downgraded to medium: it's a "review needed" heuristic, not a confirmed exploit.
5274
+ severity: "medium",
5275
+ category: "solidity",
5276
+ confidence: "low",
5277
+ languages: RUST
5278
+ },
5279
+ {
5280
+ id: "cp-sl-pyth-staleness",
5281
+ title: "Solana: Pyth/Switchboard price used without staleness check",
5282
+ description: "Pyth `get_price_unchecked` (or Switchboard `get_result`) returns a price that may be stale \u2014 the publisher could be offline. Using this for collateral pricing without checking `publish_time` is the Solana-side equivalent of the Mango oracle exploit ($114M).",
5283
+ suggestion: "Use Pyth's `get_price_no_older_than(clock, MAX_AGE)` (or `get_ema_price_no_older_than`). For Switchboard, check `result.last_update_timestamp` against `clock.unix_timestamp`.",
5284
+ multilinePattern: /(?:get_price_unchecked|get_price_no_older|get_ema_price_unchecked|get_result\s*\(\s*\))\s*\([\s\S]{0,200}?\)(?![\s\S]{0,1200}?(?:publish_time|MAX_AGE|max_age|staleness|STALENESS|unix_timestamp\s*-\s*\w+|clock\.\s*unix_timestamp))/,
5285
+ severity: "high",
5286
+ category: "solidity",
5287
+ confidence: "medium",
5288
+ languages: RUST
5289
+ },
5290
+ {
5291
+ id: "cp-sl-cpi-reentrancy",
5292
+ title: "Solana: state mutation after cross-program invocation",
5293
+ description: "Anchor checks-effects-interactions: state should be updated BEFORE invoking another program. A CPI followed by state mutation invites cross-program reentrancy, especially with callback programs.",
5294
+ suggestion: "Move state writes to BEFORE the `invoke` / `invoke_signed` / `CpiContext::new` call. Or use Anchor's account locking + a reentrancy guard flag.",
5295
+ multilinePattern: /(?:invoke|invoke_signed|CpiContext::new(?:_with_signer)?)\s*\([\s\S]{0,400}?\)\s*\?\s*;[\s\S]{0,400}?(?:\.\s*\w+\s*=\s*\w+|state\.\s*\w+\s*=|account\.\s*\w+\s*=)/,
5296
+ severity: "high",
5297
+ category: "solidity",
5298
+ confidence: "low",
5299
+ languages: RUST
5300
+ },
5301
+ {
5302
+ id: "cp-sl-arbitrary-cpi",
5303
+ title: "Solana: program passed as account, used for arbitrary CPI",
5304
+ description: "An `AccountInfo` (or untyped account) is used as the `program_id` for an `invoke()` call. An attacker who can choose the program account effectively gets arbitrary cross-program invocation \u2014 equivalent to delegatecall in EVM.",
5305
+ suggestion: "Type the program field as `Program<'info, MyProgram>` (Anchor) so the runtime enforces the program's pubkey. Or `require_keys_eq!(program.key(), expected_program_id);` before invoke.",
5306
+ multilinePattern: /invoke(?:_signed)?\s*\(\s*&Instruction\s*\{\s*program_id\s*:\s*[*\w.()]+\s*,/,
5307
+ severity: "critical",
5308
+ category: "solidity",
5309
+ confidence: "medium",
5310
+ languages: RUST
5311
+ }
5312
+ ];
5232
5313
  var ALL_RULES2 = [
5233
5314
  ...securityRules,
5234
5315
  ...solidityRules2,
@@ -5245,7 +5326,8 @@ var ALL_RULES2 = [
5245
5326
  ...tstoreRules,
5246
5327
  ...uniswapV4Rules,
5247
5328
  ...hackReplayRules,
5248
- ...modernDeFiRules
5329
+ ...modernDeFiRules,
5330
+ ...solanaRules
5249
5331
  ];
5250
5332
 
5251
5333
  // src/static/pattern-scanner.ts
@@ -5584,6 +5666,12 @@ function scanFile(relPath, content, rules, changedRanges) {
5584
5666
  if (rule.id === "cp-hack-wormhole-unchecked-signature-set" && /\b(?:EIP712|DOMAIN_SEPARATOR|_hashTypedDataV4|PERMIT_TYPEHASH|DELEGATION_TYPEHASH|ERC1271|EIP712Upgradeable)\b/.test(content)) continue;
5585
5667
  if (rule.id === "cp-sol-eip712-missing-chainid" && /\b(?:block\.chainid|chainId|chainid)\b/.test(content)) continue;
5586
5668
  if (rule.id === "cp-sol-bridge-missing-source-check" && /\b(?:EntryPoint|UserOperation|PackedUserOperation|IERC7579|IERC4337|executionCalldata|onlyEntryPoint)\b/.test(content)) continue;
5669
+ if (rule.id === "cp-sl-missing-signer-check" && /\bSigner\s*<\s*'info\s*>/.test(content)) continue;
5670
+ if (rule.id === "cp-sl-ata-confusion" && /(?:mint\s*=\s*\w+|require_keys_eq!\s*\([^)]*\.mint|token::mint_to)/.test(content)) continue;
5671
+ if (rule.id === "cp-sl-account-substitution" && /#\[account\s*\([^)]*(?:has_one|constraint|seeds)\s*=/.test(content)) continue;
5672
+ if (rule.id === "cp-sl-pyth-staleness" && /(?:get_price_no_older_than|publish_time|MAX_AGE|max_age|stale|clock\.unix_timestamp\s*-)/.test(content)) continue;
5673
+ if (rule.id === "cp-sl-arbitrary-cpi" && /Program\s*<\s*'info\s*,/.test(content)) continue;
5674
+ if (rule.id === "cp-sl-missing-owner-check" && /(?:require_keys_eq!\s*\([^)]*\.owner|assert_eq!\s*\([^)]*\.owner|Account\s*<\s*'info|AccountLoader\s*<\s*'info)/.test(content)) continue;
5587
5675
  rule.multilinePattern.lastIndex = 0;
5588
5676
  const isGlobal = rule.multilinePattern.flags.includes("g");
5589
5677
  let match;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elytrasec/engine",
3
- "version": "0.4.5",
4
- "description": "Core analysis engine for Elytra \u2014 181 detection rules including 12 famous-hack patterns, 11 rug-surface checks, 5 modern-DeFi detectors (ERC-4626 inflation, EIP-712 chainId, bridge source check, permit deadline, MEV swap), static + AI scanning, scoring.",
3
+ "version": "0.4.6",
4
+ "description": "Core analysis engine for Elytra \u2014 188 detection rules across Solidity, Solana/Anchor (Rust), JS/TS, Python, Go, IaC. Includes 12 famous-hack patterns, 11 rug-surface checks, 5 modern-DeFi detectors, 7 Solana detectors.",
5
5
  "license": "MIT",
6
6
  "author": "ElytraSec <hello@elytrasec.io>",
7
7
  "homepage": "https://elytrasec.io",