@elytrasec/engine 0.4.6 → 0.4.8

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 +100 -3
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -3985,7 +3985,9 @@ var performanceRules = [
3985
3985
  title: "Potential N+1 query \u2014 database call inside a loop",
3986
3986
  description: "A database query inside a loop makes N separate round-trips instead of 1 batch query. This causes severe performance degradation at scale.",
3987
3987
  suggestion: "Batch the queries: collect all IDs first, then execute a single WHERE IN query.",
3988
- multilinePattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?(?:\.find\(|\.findOne\(|\.findUnique\(|\.query\(|\.execute\(|SELECT\b)/,
3988
+ // Tightened: only fire on PROMISE-shaped DB calls. Array.prototype.find/map on a local var
3989
+ // is NOT an N+1 — must be `await ...` OR namespaced like `db.find`/`prisma.user.findUnique`.
3990
+ multilinePattern: /(?:for|while)\s*\([^)]*\)\s*\{[\s\S]{0,500}?(?:await\s+\w+(?:\.\w+)*\.(?:find|findOne|findUnique|query|execute)\s*\(|\b(?:db|prisma|sequelize|knex|mongoose|repo|repository)\.\w+\.\s*(?:find|findOne|findUnique|query|execute)\s*\(|SELECT\b.*FROM)/,
3989
3991
  severity: "high",
3990
3992
  category: "performance",
3991
3993
  confidence: "medium",
@@ -5308,6 +5310,99 @@ var solanaRules = [
5308
5310
  category: "solidity",
5309
5311
  confidence: "medium",
5310
5312
  languages: RUST
5313
+ },
5314
+ {
5315
+ id: "cp-sl-close-account-no-lamport-check",
5316
+ title: "Solana: account closed without verifying lamport destination",
5317
+ description: "Using `#[account(close = destination)]` or manually closing an account transfers its rent-exempt SOL to the destination. If `destination` isn't validated (constrained to the rightful owner), an attacker can drain rent into their own wallet on every close.",
5318
+ suggestion: "Constrain the close destination: `#[account(close = expected_owner, has_one = expected_owner)]`. Or verify with `require_keys_eq!(destination.key(), authority.key())` before transfer.",
5319
+ // Match only when close=X appears in a #[account(...)] block that has NO has_one / constraint
5320
+ // in the same block (tempered greedy excludes those keywords inside the brackets).
5321
+ multilinePattern: /#\[account\s*\((?:(?!has_one|constraint)[^)\n]){0,400}close\s*=\s*\w+(?:(?!has_one|constraint)[^)\n]){0,400}\)\]/,
5322
+ severity: "high",
5323
+ category: "solidity",
5324
+ confidence: "low",
5325
+ languages: RUST
5326
+ },
5327
+ {
5328
+ id: "cp-sl-lamport-overflow",
5329
+ title: "Solana: unchecked lamport arithmetic (overflow / underflow)",
5330
+ description: "Direct mutation of `account.lamports.borrow_mut() += X` or `-= X` without `checked_add` / `checked_sub` can overflow on large mints or underflow on overdraft. Solana's runtime does NOT panic on integer overflow in release mode.",
5331
+ suggestion: "Use `lamports.borrow_mut().checked_add(amount).ok_or(MyError::Overflow)?` and matching `checked_sub`. Or use `**lamports = lamports.checked_add(X)?`.",
5332
+ pattern: /\*\*[\w.]+?\.lamports\s*\.\s*borrow_mut\s*\(\s*\)\s*[+\-]=/,
5333
+ severity: "high",
5334
+ category: "solidity",
5335
+ confidence: "medium",
5336
+ languages: RUST
5337
+ },
5338
+ {
5339
+ id: "cp-sl-sysvar-substitution",
5340
+ title: "Solana: sysvar accepted as AccountInfo (substitution risk)",
5341
+ description: "Accepting `Clock`, `Rent`, `EpochSchedule`, or `SlotHashes` as raw `AccountInfo` allows an attacker to substitute a forged account holding crafted bytes. Type confusion \u2192 wrong clock values \u2192 expiry bypass.",
5342
+ suggestion: "Use typed Anchor sysvar wrappers: `pub clock: Sysvar<'info, Clock>`, `pub rent: Sysvar<'info, Rent>`. The runtime enforces the correct pubkey.",
5343
+ multilinePattern: /pub\s+(?:clock|rent|epoch_schedule|slot_hashes|recent_blockhashes|instructions)\s*:\s*AccountInfo\s*<\s*'info\s*>/,
5344
+ severity: "high",
5345
+ category: "solidity",
5346
+ confidence: "high",
5347
+ languages: RUST
5348
+ },
5349
+ {
5350
+ id: "cp-sl-pda-bump-not-stored",
5351
+ title: "Solana: PDA bump derived but not stored on account",
5352
+ description: "Calling `Pubkey::find_program_address(seeds, program_id)` recomputes the canonical bump every time \u2014 that's 32+ ed25519 ops per call. More critically, if you don't STORE the bump on the account, attackers can derive alternative PDAs with non-canonical bumps and break invariants.",
5353
+ suggestion: "Anchor: declare the seed + bump in the account constraint: `#[account(seeds = [...], bump = state.bump)]`. Store the bump on first init.",
5354
+ pattern: /Pubkey::find_program_address\s*\(/,
5355
+ severity: "low",
5356
+ category: "solidity",
5357
+ confidence: "low",
5358
+ languages: RUST
5359
+ },
5360
+ {
5361
+ id: "cp-sl-multisig-threshold-check",
5362
+ title: "Solana: multisig execution without threshold validation",
5363
+ description: "A function checks individual signer authority but never validates that the count of signers meets the multisig threshold. Squads, Realms, and similar multisig programs must enforce `signers.len() >= threshold` before executing.",
5364
+ suggestion: "Add `require!(approvals.len() as u8 >= multisig.threshold, MyError::QuorumNotReached)` before the dispatch.",
5365
+ // Allow qualified field access like multisig.threshold, state.quorum, etc.
5366
+ multilinePattern: /(?:signers|approvals|approvers|signatures|members)\s*\.\s*len\s*\(\s*\)(?![\s\S]{0,500}?(?:>=|>)\s*(?:\w+\.)?(?:threshold|quorum|min_signers|required_signers|min_approvals|m_of_n))/,
5367
+ severity: "high",
5368
+ category: "solidity",
5369
+ confidence: "low",
5370
+ languages: RUST
5371
+ }
5372
+ ];
5373
+ var modernSolidityV2Rules = [
5374
+ {
5375
+ id: "cp-sol-eip1271-replay",
5376
+ title: "EIP-1271 isValidSignature without nonce / expiry",
5377
+ description: "Smart-contract signatures via `isValidSignature` (ERC-1271) that don't bind to a nonce + expiry can be replayed indefinitely. Permit2 leak-class attacks of 2024 exploited exactly this. Once a signature is leaked, it's valid forever and on every chain.",
5378
+ suggestion: "Bind every accepted signature to: (a) a per-account nonce, (b) a deadline / expiry timestamp, AND (c) `block.chainid` in the typed-data domain. Increment nonce on successful verify.",
5379
+ multilinePattern: /function\s+isValidSignature\s*\([^)]*\)\s*(?:external|public)[^{;]*\{(?![\s\S]{0,1500}?(?:nonce|deadline|expiry|chainid|usedSig|_consumeNonce|_useNonce))/,
5380
+ severity: "high",
5381
+ category: "solidity",
5382
+ confidence: "medium",
5383
+ languages: SOL
5384
+ },
5385
+ {
5386
+ id: "cp-sol-safemint-reentrancy",
5387
+ title: "ERC-721 _safeMint reentrancy via onERC721Received callback",
5388
+ description: "`_safeMint` calls the recipient's `onERC721Received` hook. If state (totalSupply, balances, mint cap) is updated AFTER the safeMint, the recipient contract can re-enter and bypass the cap. Several memecoin / NFT mints have lost their entire whitelist allocation this way.",
5389
+ suggestion: "Apply checks-effects-interactions: update mintCount, totalSupply, whitelist-spent BEFORE calling `_safeMint`. Or use `_mint` (no callback) when you don't need ERC-721-receiver compatibility, or add `nonReentrant`.",
5390
+ multilinePattern: /_safeMint\s*\([^)]*\)\s*;[\s\S]{0,300}?(?:totalSupply|_mintedCount|mintedPerUser|whitelist\s*\[[^\]]*\]\s*=|_balanceOf\s*\[[^\]]*\]\s*=|\.\s*\w+\s*\+=\s*1)/,
5391
+ severity: "high",
5392
+ category: "solidity",
5393
+ confidence: "medium",
5394
+ languages: SOL
5395
+ },
5396
+ {
5397
+ id: "cp-sol-voting-snapshot-bypass",
5398
+ title: "Voting power read from spot balanceOf (snapshot manipulation)",
5399
+ description: "A governance proposal reads voting weight from `balanceOf(voter)` at vote time. Attacker flash-borrows the governance token, votes, returns the loan in the same tx. This is distinct from the Beanstalk same-block execute bug \u2014 here even a 24h timelock won't help because the bad vote is already counted.",
5400
+ suggestion: "Use `getPriorVotes(voter, proposalSnapshotBlock)` (Compound), `ERC20Votes.getPastVotes`, or maintain a delegated-balance snapshot via Comp/OZ Votes. Never use `balanceOf` for voting weight.",
5401
+ multilinePattern: /function\s+(?:castVote|vote|castVoteWithReason|_castVote)\s*\([^)]*\)[^{;]*\{[\s\S]{0,800}?balanceOf\s*\(\s*(?:msg\.sender|voter|\w+)\s*\)(?![\s\S]{0,400}?(?:getPriorVotes|getPastVotes|getVotes|snapshot|_snapshot|checkpoints))/,
5402
+ severity: "critical",
5403
+ category: "solidity",
5404
+ confidence: "medium",
5405
+ languages: SOL
5311
5406
  }
5312
5407
  ];
5313
5408
  var ALL_RULES2 = [
@@ -5327,7 +5422,8 @@ var ALL_RULES2 = [
5327
5422
  ...uniswapV4Rules,
5328
5423
  ...hackReplayRules,
5329
5424
  ...modernDeFiRules,
5330
- ...solanaRules
5425
+ ...solanaRules,
5426
+ ...modernSolidityV2Rules
5331
5427
  ];
5332
5428
 
5333
5429
  // src/static/pattern-scanner.ts
@@ -5364,7 +5460,7 @@ function isDocsFile(filePath) {
5364
5460
  }
5365
5461
  function isCommentLine(line) {
5366
5462
  const trimmed = line.trim();
5367
- return /^(?:\/\/|#|\/?\*|<!--)/.test(trimmed);
5463
+ return /^(?!\*\*\w)(?:\/\/|#|\/?\*|<!--)/.test(trimmed);
5368
5464
  }
5369
5465
  function isImportLine(line) {
5370
5466
  const trimmed = line.trim();
@@ -5672,6 +5768,7 @@ function scanFile(relPath, content, rules, changedRanges) {
5672
5768
  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
5769
  if (rule.id === "cp-sl-arbitrary-cpi" && /Program\s*<\s*'info\s*,/.test(content)) continue;
5674
5770
  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;
5771
+ if (rule.id === "cp-sol-voting-snapshot-bypass" && /(?:getPriorVotes|getPastVotes|getVotes|_snapshot|checkpoints|ERC20Votes|VotesUpgradeable|delegateBySig)/.test(content)) continue;
5675
5772
  rule.multilinePattern.lastIndex = 0;
5676
5773
  const isGlobal = rule.multilinePattern.flags.includes("g");
5677
5774
  let match;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elytrasec/engine",
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.",
3
+ "version": "0.4.8",
4
+ "description": "Core analysis engine for Elytra \u2014 196 detection rules across Solidity, Solana/Anchor (Rust), JS/TS, Python, Go, IaC. 12 famous-hack patterns, 11 rug-surface, 5 modern-DeFi + 3 modern-Solidity-v2, 12 Solana detectors.",
5
5
  "license": "MIT",
6
6
  "author": "ElytraSec <hello@elytrasec.io>",
7
7
  "homepage": "https://elytrasec.io",