@elytrasec/engine 0.4.4 → 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 +157 -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 = ["*"];
@@ -5164,6 +5165,151 @@ var rugSurfaceRules = [
5164
5165
  languages: SOL
5165
5166
  }
5166
5167
  ];
5168
+ var modernDeFiRules = [
5169
+ {
5170
+ id: "cp-sol-erc4626-inflation",
5171
+ title: "ERC-4626 vault: missing inflation-attack protection",
5172
+ description: "ERC-4626 deposit/mint computes shares as `assets * totalSupply / totalAssets`. On an empty vault, an attacker who is the first depositor can deposit 1 wei, then DIRECTLY transfer a large amount of underlying to the vault \u2014 inflating the share/asset ratio. Subsequent depositors get rounded to ZERO shares for any deposit smaller than the inflated rate. Cream, Hundred, and many forks lost funds this way.",
5173
+ suggestion: "Use OpenZeppelin's ERC4626 v5+ which adds 1e6 virtual shares/assets (decimal offset). OR: in deposit(), require msg.sender to mint to a dead address first (initial burn of N shares). OR: enforce a minimum total-supply invariant.",
5174
+ multilinePattern: /function\s+(?:deposit|mint)\s*\([^)]*\)\s*(?:external|public)[^{;]*\{[\s\S]{0,1500}?(?:totalSupply|_totalSupply)\s*\([^)]*\)\s*[*/]\s*\w(?![\s\S]{0,800}?(?:_decimalsOffset|virtualShares|VIRTUAL_SHARES|MINIMUM_LIQUIDITY|10\s*\*\*\s*\d|DEAD_SHARES|_burnDead|dead\s*\(|initialBurn))/,
5175
+ severity: "high",
5176
+ category: "solidity",
5177
+ confidence: "medium",
5178
+ languages: SOL
5179
+ },
5180
+ {
5181
+ id: "cp-sol-eip712-missing-chainid",
5182
+ title: "EIP-712 domain separator computed without chainId",
5183
+ description: "An EIP-712 typed-data signature scheme that omits `block.chainid` from the DOMAIN_SEPARATOR allows the same signature to be replayed on a fork or testnet. Several bridge and permit exploits have come from this exact gap.",
5184
+ suggestion: "Always include `block.chainid` in the EIP-712 domain hash, OR inherit from OpenZeppelin's EIP712 which handles it. Recompute the separator when chainid changes (fork detection).",
5185
+ // Catches the domain-separator computation. File-level FP filter in pattern-scanner.ts
5186
+ // suppresses this rule entirely if the file has any chainid reference anywhere.
5187
+ multilinePattern: /(?:DOMAIN_SEPARATOR|_domainSeparator|domainSeparator)\s*=\s*keccak256\s*\(\s*abi\.encode\s*\([\s\S]{20,500}?\)\s*\)/,
5188
+ severity: "high",
5189
+ category: "solidity",
5190
+ confidence: "medium",
5191
+ languages: SOL
5192
+ },
5193
+ {
5194
+ id: "cp-sol-swap-zero-deadline",
5195
+ title: "Swap call with zero or absent deadline (MEV vulnerable)",
5196
+ description: "Calling a Uniswap V2/V3/V4 router swap with deadline = 0, deadline = block.timestamp (no protection), or deadline = type(uint256).max means the transaction can sit in the mempool indefinitely and be sandwiched whenever the price moves against you. Users lose money to MEV bots.",
5197
+ suggestion: "Enforce a real deadline: `deadline = block.timestamp + 15 minutes` at minimum. Pass it from the user, not computed inside the function. Reject calls with deadline > block.timestamp + 1 hour.",
5198
+ // Catches both named-arg (deadline: 0) and positional last-arg = 0 or block.timestamp or type(uint).max.
5199
+ // Multiline + tolerant of nested parens (address(this), etc.). Catches positional last-arg
5200
+ // = 0, block.timestamp, or type(uint).max as the deadline.
5201
+ multilinePattern: /(?:swapExactTokensForTokens|swapTokensForExactTokens|exactInput|exactOutput|swap[A-Z]\w*)\s*\([\s\S]{0,400}?,\s*(?:0|block\.timestamp|type\s*\(\s*uint256\s*\)\s*\.\s*max)\s*\)\s*;/,
5202
+ severity: "medium",
5203
+ category: "solidity",
5204
+ confidence: "low",
5205
+ languages: SOL
5206
+ },
5207
+ {
5208
+ id: "cp-sol-bridge-missing-source-check",
5209
+ title: "Cross-chain receive function: missing source-chain / trusted-remote check",
5210
+ description: "LayerZero `lzReceive`, Axelar `_execute`, Hyperlane `handle`, Wormhole `receiveMessage` and similar cross-chain receivers MUST verify (a) the source chain ID and (b) the trusted-remote sender address. Without these checks ANY contract on any chain can forge a message and trigger the receive logic \u2014 leading to unauthorized mint/withdraw of bridged assets.",
5211
+ suggestion: "Add explicit checks: `require(_srcChainId == trustedSrcChain)` and `require(keccak256(_srcAddress) == keccak256(trustedRemote))`. For LayerZero use `lzApp.setTrustedRemote`. For Axelar verify `sourceChain` + `sourceAddress`.",
5212
+ // Bridge-specific function names; file-level filter in pattern-scanner.ts skips ERC-4337
5213
+ // and similar accounts. Negative lookahead for trusted-remote / source-check in body.
5214
+ multilinePattern: /function\s+(?:lzReceive|nonblockingLzReceive|_nonblockingLzReceive|_handleMessage|receiveMessage|handle)\s*\([^)]*\)\s*(?:external|internal|public)[^{;]*\{(?![\s\S]{0,1500}?(?:trustedRemote|_trustedRemoteLookup|sourceChain\s*==|_srcChainId\s*==|trustedSrcChain|trustedSender|_validateMessageSource|_authorizeSource|onlyEndpoint|onlyMailbox|onlyGateway|interchainSecurityModule))/,
5215
+ severity: "critical",
5216
+ category: "solidity",
5217
+ confidence: "medium",
5218
+ languages: SOL
5219
+ },
5220
+ {
5221
+ id: "cp-sol-permit2-no-deadline-check",
5222
+ title: "Permit signature accepted without deadline / expiry check",
5223
+ description: "EIP-2612 permit() (and similar typed-data signature endpoints) must check that `deadline >= block.timestamp`. Without this, a leaked signature is valid forever. Several memecoin-launchpad bugs of 2024 leaked permits and lost early-mover allocations.",
5224
+ suggestion: 'Add `require(deadline >= block.timestamp, "Permit expired")` at the top of permit-like functions. Reject if deadline is type(uint256).max (suggests caller forgot to set one).',
5225
+ // Also accept assembly-style timestamp/deadline check (Solady pattern: gt(timestamp(), deadline))
5226
+ multilinePattern: /function\s+(?:permit|permitWithSig|permitTransfer|permitTransferFrom)\s*\([^)]*deadline[^)]*\)[^{;]*\{(?![\s\S]{0,1500}?(?:deadline\s*[><]=?\s*block\.timestamp|block\.timestamp\s*[<>]=?\s*deadline|require\s*\([^)]*deadline|gt\s*\(\s*timestamp\s*\(\s*\)\s*,\s*deadline|PermitExpired|ExpiredSignature|EXPIRED))/,
5227
+ severity: "high",
5228
+ category: "solidity",
5229
+ confidence: "medium",
5230
+ languages: SOL
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
+ ];
5167
5313
  var ALL_RULES2 = [
5168
5314
  ...securityRules,
5169
5315
  ...solidityRules2,
@@ -5179,7 +5325,9 @@ var ALL_RULES2 = [
5179
5325
  ...eip7702Rules,
5180
5326
  ...tstoreRules,
5181
5327
  ...uniswapV4Rules,
5182
- ...hackReplayRules
5328
+ ...hackReplayRules,
5329
+ ...modernDeFiRules,
5330
+ ...solanaRules
5183
5331
  ];
5184
5332
 
5185
5333
  // src/static/pattern-scanner.ts
@@ -5516,6 +5664,14 @@ function scanFile(relPath, content, rules, changedRanges) {
5516
5664
  if (rule.id === "cp-clean-callback-hell" && isTestFile(relPath)) continue;
5517
5665
  if (rule.id === "cp-sec-command-injection" && isScriptDir(relPath)) continue;
5518
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;
5667
+ if (rule.id === "cp-sol-eip712-missing-chainid" && /\b(?:block\.chainid|chainId|chainid)\b/.test(content)) continue;
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;
5519
5675
  rule.multilinePattern.lastIndex = 0;
5520
5676
  const isGlobal = rule.multilinePattern.flags.includes("g");
5521
5677
  let match;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elytrasec/engine",
3
- "version": "0.4.4",
4
- "description": "Core analysis engine for Elytra \u2014 173 detection rules including 12 famous-hack patterns and 11 rug-surface checks, 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",