@elytrasec/engine 0.4.0 → 0.4.1

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 (3) hide show
  1. package/dist/index.js +25 -21
  2. package/package.json +12 -3
  3. package/dist/index.d.ts +0 -1358
package/dist/index.js CHANGED
@@ -3105,7 +3105,7 @@ var securityRules = [
3105
3105
  title: "Form without CSRF protection",
3106
3106
  description: "HTML form with method=POST but no visible CSRF token field. This may be vulnerable to cross-site request forgery.",
3107
3107
  suggestion: "Add a hidden CSRF token field to the form or use a framework-provided CSRF middleware.",
3108
- pattern: /method\s*=\s*["']post["'][^>]*>(?![\s\S]{0,500}csrf)/i,
3108
+ multilinePattern: /method\s*=\s*["']post["'][^>]*>(?![\s\S]{0,500}csrf)/i,
3109
3109
  severity: "medium",
3110
3110
  category: "security",
3111
3111
  confidence: "low",
@@ -3161,7 +3161,7 @@ var securityRules = [
3161
3161
  title: "XML parsing without entity restriction (XXE)",
3162
3162
  description: "XML parsers that allow external entities can be exploited to read local files or perform SSRF.",
3163
3163
  suggestion: "Disable external entity processing: set noent: false, or use defusedxml in Python.",
3164
- pattern: /(?:parseXml|parseString|DOMParser|xml2js|libxmljs|XMLParser)\s*\((?![\s\S]*(?:noent:\s*false|resolve_entities:\s*false))/,
3164
+ multilinePattern: /(?:parseXml|parseString|DOMParser|xml2js|libxmljs|XMLParser)\s*\((?![\s\S]*(?:noent:\s*false|resolve_entities:\s*false))/,
3165
3165
  severity: "high",
3166
3166
  category: "security",
3167
3167
  confidence: "medium",
@@ -3211,7 +3211,7 @@ var securityRules = [
3211
3211
  title: "Mass assignment via spread of user input",
3212
3212
  description: "Spreading req.body directly into a database create/update allows attackers to set arbitrary fields (e.g. isAdmin).",
3213
3213
  suggestion: "Explicitly pick allowed fields instead of spreading the entire request body.",
3214
- pattern: /(?:create|update|insert|save|upsert)\s*\(\s*\{[\s\S]{0,50}\.\.\.(?:req\.body|req\.query|body|input)\b/,
3214
+ multilinePattern: /(?:create|update|insert|save|upsert)\s*\(\s*\{[\s\S]{0,50}\.\.\.(?:req\.body|req\.query|body|input)\b/,
3215
3215
  severity: "high",
3216
3216
  category: "security",
3217
3217
  confidence: "medium",
@@ -3278,7 +3278,7 @@ var securityRules = [
3278
3278
  title: "Cookie set without security flags",
3279
3279
  description: "Cookies set without Secure, HttpOnly, or SameSite flags are vulnerable to interception and XSS theft.",
3280
3280
  suggestion: "Set cookies with { secure: true, httpOnly: true, sameSite: 'strict' } flags.",
3281
- pattern: /(?:res\.cookie|setCookie|set-cookie|document\.cookie)\s*(?:\(|=)(?![\s\S]{0,100}(?:secure|httpOnly|SameSite))/i,
3281
+ multilinePattern: /(?:res\.cookie|setCookie|set-cookie|document\.cookie)\s*(?:\(|=)(?![\s\S]{0,100}(?:secure|httpOnly|SameSite))/i,
3282
3282
  severity: "medium",
3283
3283
  category: "security",
3284
3284
  confidence: "medium",
@@ -3306,7 +3306,7 @@ var securityRules = [
3306
3306
  title: "CORS credentials with wildcard origin",
3307
3307
  description: "Enabling credentials with a wildcard or reflected origin allows any site to make authenticated cross-origin requests.",
3308
3308
  suggestion: "When using credentials: true, specify explicit trusted origins instead of '*' or reflecting the Origin header.",
3309
- pattern: /credentials\s*:\s*true[\s\S]{0,200}origin\s*:\s*(?:["']\*["']|req\.headers\.origin|true)/,
3309
+ multilinePattern: /credentials\s*:\s*true[\s\S]{0,200}origin\s*:\s*(?:["']\*["']|req\.headers\.origin|true)/,
3310
3310
  severity: "high",
3311
3311
  category: "security",
3312
3312
  confidence: "high",
@@ -3477,7 +3477,7 @@ var solidityRules2 = [
3477
3477
  title: "balanceOf used for pricing without flash loan guards",
3478
3478
  description: "Using balanceOf() for pricing calculations is vulnerable to flash loan manipulation.",
3479
3479
  suggestion: "Use time-weighted average prices (TWAP) or Chainlink oracles instead of spot balanceOf for pricing.",
3480
- pattern: /balanceOf\s*\([^)]*\)[\s\S]{0,20}(?:\*|\/)/,
3480
+ multilinePattern: /balanceOf\s*\([^)]*\)[\s\S]{0,20}(?:\*|\/)/,
3481
3481
  severity: "high",
3482
3482
  category: "security",
3483
3483
  confidence: "medium",
@@ -3537,7 +3537,7 @@ var javaRules = [
3537
3537
  title: "XPath injection via string concatenation",
3538
3538
  description: "Building XPath queries with string concatenation allows injection attacks.",
3539
3539
  suggestion: "Use XPath parameterization via XPathExpression or sanitize input.",
3540
- pattern: /(?:XPath|xpath)[\s\S]{0,50}\.(?:evaluate|compile)\s*\(\s*(?:["'][^"']*["']\s*\+|.*\+\s*["'])/,
3540
+ multilinePattern: /(?:XPath|xpath)[\s\S]{0,50}\.(?:evaluate|compile)\s*\(\s*(?:["'][^"']*["']\s*\+|.*\+\s*["'])/,
3541
3541
  severity: "high",
3542
3542
  category: "security",
3543
3543
  confidence: "medium",
@@ -3931,7 +3931,7 @@ var performanceRules = [
3931
3931
  title: "Potential N+1 query \u2014 database call inside a loop",
3932
3932
  description: "A database query inside a loop makes N separate round-trips instead of 1 batch query. This causes severe performance degradation at scale.",
3933
3933
  suggestion: "Batch the queries: collect all IDs first, then execute a single WHERE IN query.",
3934
- pattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?(?:\.find\(|\.findOne\(|\.findUnique\(|\.query\(|\.execute\(|SELECT\b)/,
3934
+ multilinePattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?(?:\.find\(|\.findOne\(|\.findUnique\(|\.query\(|\.execute\(|SELECT\b)/,
3935
3935
  severity: "high",
3936
3936
  category: "performance",
3937
3937
  confidence: "medium",
@@ -3942,7 +3942,7 @@ var performanceRules = [
3942
3942
  title: "Nested iteration \u2014 Array search inside a loop",
3943
3943
  description: "Using Array.find/filter/some/indexOf inside a loop is O(n*m). Use a Map or Set for O(n) lookups.",
3944
3944
  suggestion: "Build a Map or Set from the inner array before the loop, then use .get()/.has() for O(1) lookup.",
3945
- pattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?\.(?:find|filter|some|indexOf|includes)\s*\(/,
3945
+ multilinePattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?\.(?:find|filter|some|indexOf|includes)\s*\(/,
3946
3946
  severity: "low",
3947
3947
  category: "performance",
3948
3948
  confidence: "medium",
@@ -3965,7 +3965,7 @@ var performanceRules = [
3965
3965
  title: "RegExp construction inside a loop",
3966
3966
  description: "Creating a new RegExp object on every iteration is wasteful. Regex compilation is expensive.",
3967
3967
  suggestion: "Move the RegExp construction outside the loop and reuse the compiled pattern.",
3968
- pattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?new\s+RegExp\s*\(/,
3968
+ multilinePattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?new\s+RegExp\s*\(/,
3969
3969
  severity: "low",
3970
3970
  category: "performance",
3971
3971
  confidence: "medium",
@@ -3976,7 +3976,7 @@ var performanceRules = [
3976
3976
  title: "JSON.parse inside a loop",
3977
3977
  description: "Parsing JSON inside a loop is CPU-intensive. If the same data is re-parsed, cache the result.",
3978
3978
  suggestion: "Parse the JSON once before the loop and reuse the result, or use streaming JSON parsing for large datasets.",
3979
- pattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?JSON\.parse\s*\(/,
3979
+ multilinePattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?JSON\.parse\s*\(/,
3980
3980
  severity: "low",
3981
3981
  category: "performance",
3982
3982
  confidence: "medium",
@@ -3987,7 +3987,7 @@ var performanceRules = [
3987
3987
  title: "Sequential await inside a loop",
3988
3988
  description: "Using await inside a loop executes async operations sequentially. Independent operations can run in parallel with Promise.all.",
3989
3989
  suggestion: "Collect promises in an array and use Promise.all() or Promise.allSettled() for parallel execution.",
3990
- pattern: /(?:for|while)\s*\([\s\S]*?await\s+/,
3990
+ multilinePattern: /(?:for|while)\s*\([\s\S]*?await\s+/,
3991
3991
  severity: "low",
3992
3992
  category: "performance",
3993
3993
  confidence: "low",
@@ -4719,7 +4719,7 @@ var uniswapV4Rules = [
4719
4719
  title: "Uniswap v4 hook: BalanceDelta returned without settle/take",
4720
4720
  description: "Hooks that return a modified `BalanceDelta` must ensure the delta is fully consumed (settled or taken) within the same unlock cycle. An unconsumed delta causes `CurrencyNotSettled` revert; partial consumption can silently strand tokens.",
4721
4721
  suggestion: "Ensure `poolManager.settle()` or `poolManager.take()` is called for both currency0 and currency1 before returning from the unlock callback.",
4722
- pattern: /returns\s*\([^)]*BalanceDelta[^)]*\)(?![\s\S]{0,800}?(?:settle|\.take)\s*\()/,
4722
+ multilinePattern: /returns\s*\([^)]*BalanceDelta[^)]*\)(?![\s\S]{0,800}?(?:settle|\.take)\s*\()/,
4723
4723
  severity: "high",
4724
4724
  category: "solidity",
4725
4725
  confidence: "low",
@@ -4730,7 +4730,7 @@ var uniswapV4Rules = [
4730
4730
  title: "Uniswap v4 hook: overly broad hook permissions",
4731
4731
  description: "Hook permissions are set at deployment via the address bit-flags and cannot be changed. Requesting permissions you don't need (e.g. `BEFORE_SWAP_RETURNS_DELTA` when you never return a delta) increases attack surface and gas costs for every pool interaction.",
4732
4732
  suggestion: "Return only the minimum required `Hooks.Permissions` from `getHookPermissions()`. Audit each permission flag against your actual callback implementations.",
4733
- pattern: /getHookPermissions\s*\(\s*\)\s*(?:public|external|override)[^{]*\{[\s\S]{0,300}?true/,
4733
+ multilinePattern: /getHookPermissions\s*\(\s*\)\s*(?:public|external|override)[^{]*\{[\s\S]{0,300}?true/,
4734
4734
  severity: "medium",
4735
4735
  category: "solidity",
4736
4736
  confidence: "low",
@@ -4789,7 +4789,7 @@ var hackReplayRules = [
4789
4789
  title: "Radiant hack pattern \u2014 single-step ownership transfer (no Ownable2Step)",
4790
4790
  description: "transferOwnership is implemented as a single-call function \u2014 the new owner takes effect immediately. This is the same pattern the Radiant Capital ($53M, Oct 2024) attackers exploited after compromising a multisig signer's UI: ownership flipped before anyone could react. A two-step or timelocked pattern would have created a defensive window.",
4791
4791
  suggestion: "Inherit from OpenZeppelin Ownable2Step (requires acceptOwnership from the new owner) AND gate it behind a timelock for production deployments. Never let a single signature transfer ownership atomically.",
4792
- pattern: /function\s+transferOwnership\s*\(\s*address\s+\w+\s*\)\s*(?:public|external)(?![\s\S]{0,400}?(?:pendingOwner|_pendingOwner|acceptOwnership|Ownable2Step|timelock|Timelock|TimelockController))/,
4792
+ multilinePattern: /function\s+transferOwnership\s*\(\s*address\s+\w+\s*\)\s*(?:public|external)(?![\s\S]{0,400}?(?:pendingOwner|_pendingOwner|acceptOwnership|Ownable2Step|timelock|Timelock|TimelockController))/,
4793
4793
  severity: "high",
4794
4794
  category: "hack-replay",
4795
4795
  confidence: "medium",
@@ -4841,7 +4841,7 @@ var hackReplayRules = [
4841
4841
  title: "Beanstalk hack pattern \u2014 governance execute() with no timelock between vote and call",
4842
4842
  description: "A governance contract exposes an execute() / executeProposal() / propose() function callable in the same block as voting. This is the exact $182M Beanstalk vector (April 2022): an attacker flash-loaned the governance token, voted yes on a self-draining proposal, and called execute() in the same transaction. A timelock between successful vote and execution would have made the flash-loan economically pointless.",
4843
4843
  suggestion: "Add a queue/execute split: successful proposals enter a TimelockController with a minimum delay (24-48h is standard). Require execute() to verify block.timestamp >= queuedAt + delay. Never let voting power, proposal acceptance, and code execution happen atomically.",
4844
- pattern: /function\s+(?:execute(?:Proposal)?|propose(?:AndExecute)?)\s*\([^)]*\)\s*(?:external|public)(?:\s+payable)?(?:\s+returns)?[^{]*\{(?![\s\S]{0,600}?(?:timelock|Timelock|TimelockController|queued|block\.timestamp\s*[><]=?\s*\w+\s*\+))/,
4844
+ multilinePattern: /function\s+(?:execute(?:Proposal)?|propose(?:AndExecute)?)\s*\([^)]*\)\s*(?:external|public)(?:\s+payable)?(?:\s+returns)?[^{]*\{(?![\s\S]{0,600}?(?:timelock|Timelock|TimelockController|queued|block\.timestamp\s*[><]=?\s*\w+\s*\+))/,
4845
4845
  severity: "critical",
4846
4846
  category: "hack-replay",
4847
4847
  confidence: "low",
@@ -4857,7 +4857,7 @@ var hackReplayRules = [
4857
4857
  title: "Multichain hack pattern \u2014 bridge withdraw/unlock gated by single owner role",
4858
4858
  description: "A bridge function (withdraw, unlock, releaseTo, claim, mintBridged) is gated only by onlyOwner or a single-address admin check. This was the structural failure behind the $126M Multichain collapse (July 2023): the CEO's MPC key controlled bridge outflows, and when he was detained, attackers (or insiders) drained the bridge. Bridge withdraw functions are the highest-value attack surface in crypto and must be multi-party.",
4859
4859
  suggestion: "Require a multi-sig (Safe / Gnosis), a multi-party MPC (TSS), or an N-of-M validator set before any bridge outflow. Never let a single private key sign a withdraw of bridged assets. Add per-asset and per-block outflow caps as a defense-in-depth limit.",
4860
- pattern: /function\s+(?:withdraw|unlock|releaseTo|releaseFor|claim|mintBridged|bridgeOut|relayOut)\s*\([^)]*\)\s*(?:external|public)\s+(?:onlyOwner|onlyAdmin|onlyMPC)(?![\s\S]{0,200}?(?:multisig|Multisig|threshold|N_OF_M|validators|attestors))/,
4860
+ multilinePattern: /function\s+(?:withdraw|unlock|releaseTo|releaseFor|claim|mintBridged|bridgeOut|relayOut)\s*\([^)]*\)\s*(?:external|public)\s+(?:onlyOwner|onlyAdmin|onlyMPC)(?![\s\S]{0,200}?(?:multisig|Multisig|threshold|N_OF_M|validators|attestors))/,
4861
4861
  severity: "critical",
4862
4862
  category: "hack-replay",
4863
4863
  confidence: "medium",
@@ -4909,7 +4909,7 @@ var hackReplayRules = [
4909
4909
  title: "Wormhole hack pattern \u2014 guardian/validator signature accepted without strict set verification",
4910
4910
  description: "A function verifies a signature against a validator/guardian set but does not strictly check that the set hash matches the expected current set (or compares against a default/empty value). The $325M Wormhole hack (Feb 2022) exploited a stub `verify_signatures` path that accepted forged signatures because the validator set comparison was missing/insecure. ecrecover-based signature schemes need every validator set rotation tracked by an explicit hash check.",
4911
4911
  suggestion: "Verify the signed message includes a strict hash of the current validator set. Reject signatures where the recovered signer is not in the active set. Never use `default!()` or zero-initialized guardian arrays in signature paths.",
4912
- pattern: /(?:ecrecover|recover)\s*\([\s\S]{0,400}?\)(?![\s\S]{0,300}?(?:guardianSet|validatorSet|currentSetHash|setHash|require\s*\([^)]*==\s*expected))/,
4912
+ multilinePattern: /(?:ecrecover|recover)\s*\([\s\S]{0,400}?\)(?![\s\S]{0,300}?(?:guardianSet|validatorSet|currentSetHash|setHash|require\s*\([^)]*==\s*expected))/,
4913
4913
  severity: "critical",
4914
4914
  category: "hack-replay",
4915
4915
  confidence: "low",
@@ -4945,7 +4945,7 @@ var hackReplayRules = [
4945
4945
  // Tightened: require the call to appear in a function whose name implies
4946
4946
  // lending / collateral / liquidation context (the actual hack surface).
4947
4947
  // Plain DEX pool internals like Uniswap getReserves are not at risk.
4948
- pattern: /function\s+(?:borrow\w*|liquidat\w*|isHealthy|collateralValue|getAccountHealth|maxBorrow|withdrawCollateral|getBorrowable)[\s\S]{0,600}?\b(?:getPrice|latestAnswer|peek|read)\s*\((?![\s\S]{0,400}?(?:twap|TWAP|deviation|maxDeviation|stalenessThreshold|secondaryPrice|sanity))/,
4948
+ multilinePattern: /function\s+(?:borrow\w*|liquidat\w*|isHealthy|collateralValue|getAccountHealth|maxBorrow|withdrawCollateral|getBorrowable)[\s\S]{0,600}?\b(?:getPrice|latestAnswer|peek|read)\s*\((?![\s\S]{0,400}?(?:twap|TWAP|deviation|maxDeviation|stalenessThreshold|secondaryPrice|sanity))/,
4949
4949
  severity: "medium",
4950
4950
  category: "hack-replay",
4951
4951
  confidence: "low",
@@ -5046,7 +5046,7 @@ var rugSurfaceRules = [
5046
5046
  title: "Renounceable ownership without evidence of renunciation",
5047
5047
  description: "Contract inherits Ownable / OwnableUpgradeable but the constructor / initializer doesn't transfer ownership to a known-safe address (zero, multisig, timelock). Owner power may still be active.",
5048
5048
  suggestion: "Either renounce ownership in the constructor (acceptably for fully-immutable contracts), or transfer to a multisig. Document this explicitly.",
5049
- pattern: /(?:contract|abstract\s+contract)\s+\w+[\s\S]{0,500}?\bis\s+[\w,\s]*?Ownable(?:Upgradeable)?\b(?![\s\S]{0,2000}?(?:renounceOwnership\s*\(\)|transferOwnership\s*\(\s*address\(0x|TimelockController|Safe\s*\())/,
5049
+ multilinePattern: /(?:contract|abstract\s+contract)\s+\w+[\s\S]{0,500}?\bis\s+[\w,\s]*?Ownable(?:Upgradeable)?\b(?![\s\S]{0,2000}?(?:renounceOwnership\s*\(\)|transferOwnership\s*\(\s*address\(0x|TimelockController|Safe\s*\())/,
5050
5050
  severity: "medium",
5051
5051
  category: "rug-surface",
5052
5052
  confidence: "low",
@@ -5068,7 +5068,7 @@ var rugSurfaceRules = [
5068
5068
  title: "Privileged modifier without visible role check in body",
5069
5069
  description: "A modifier name doesn't suggest privilege (e.g. `lock`, `safe`, `nonReentrant`-shaped names) but its body restricts access. Auditors miss these; users assume the function is open.",
5070
5070
  suggestion: "Rename modifier to make privilege explicit (e.g. `onlyOperator`). Or move check inline so reviewers see it in the function body.",
5071
- pattern: /modifier\s+(?!onlyOwner|onlyAdmin|onlyRole|nonReentrant|whenNotPaused|whenPaused|initializer|reinitializer)(\w+)\s*\(\s*\)\s*\{[\s\S]{0,200}?require\s*\(\s*msg\.sender\s*==\s*\w+/,
5071
+ multilinePattern: /modifier\s+(?!onlyOwner|onlyAdmin|onlyRole|nonReentrant|whenNotPaused|whenPaused|initializer|reinitializer)(\w+)\s*\(\s*\)\s*\{[\s\S]{0,200}?require\s*\(\s*msg\.sender\s*==\s*\w+/,
5072
5072
  severity: "medium",
5073
5073
  category: "rug-surface",
5074
5074
  confidence: "low",
@@ -5395,6 +5395,7 @@ function scanFile(relPath, content, rules, changedRanges) {
5395
5395
  const applicableRules = rules.filter((r) => ruleAppliesToFile(r, relPath));
5396
5396
  if (applicableRules.length === 0) return findings;
5397
5397
  for (const rule of applicableRules) {
5398
+ if (!rule.pattern) continue;
5398
5399
  for (let i = 0; i < lines.length; i++) {
5399
5400
  const lineNumber = i + 1;
5400
5401
  const line = lines[i];
@@ -5426,8 +5427,10 @@ function scanFile(relPath, content, rules, changedRanges) {
5426
5427
  if (rule.id === "cp-clean-callback-hell" && isTestFile(relPath)) continue;
5427
5428
  if (rule.id === "cp-sec-command-injection" && isScriptDir(relPath)) continue;
5428
5429
  rule.multilinePattern.lastIndex = 0;
5430
+ const isGlobal = rule.multilinePattern.flags.includes("g");
5429
5431
  let match;
5430
5432
  while ((match = rule.multilinePattern.exec(content)) !== null) {
5433
+ const breakAfter = !isGlobal;
5431
5434
  const textBefore = content.slice(0, match.index);
5432
5435
  const startLine = textBefore.split("\n").length;
5433
5436
  const matchLines = match[0].split("\n").length;
@@ -5453,6 +5456,7 @@ function scanFile(relPath, content, rules, changedRanges) {
5453
5456
  if (match[0].length === 0) {
5454
5457
  rule.multilinePattern.lastIndex++;
5455
5458
  }
5459
+ if (breakAfter) break;
5456
5460
  }
5457
5461
  }
5458
5462
  if (rules.some((r) => r.category === "code-cleaning")) {
package/package.json CHANGED
@@ -1,12 +1,21 @@
1
1
  {
2
2
  "name": "@elytrasec/engine",
3
- "version": "0.4.0",
4
- "description": "Core analysis engine for Elytra 173 detection rules including 12 famous-hack patterns and 11 rug-surface checks, static + AI scanning, scoring.",
3
+ "version": "0.4.1",
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.",
5
5
  "license": "MIT",
6
6
  "author": "ElytraSec <hello@elytrasec.io>",
7
7
  "homepage": "https://elytrasec.io",
8
8
  "bugs": "https://elytrasec.io/agents",
9
- "keywords": ["security", "scanner", "static-analysis", "code-review", "vulnerability-detection", "solidity", "defi", "rug-surface"],
9
+ "keywords": [
10
+ "security",
11
+ "scanner",
12
+ "static-analysis",
13
+ "code-review",
14
+ "vulnerability-detection",
15
+ "solidity",
16
+ "defi",
17
+ "rug-surface"
18
+ ],
10
19
  "engines": {
11
20
  "node": ">=20"
12
21
  },