@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.
- package/dist/index.js +25 -21
- package/package.json +12 -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "Core analysis engine for Elytra
|
|
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": [
|
|
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
|
},
|