@elytrasec/engine 0.4.0 → 0.4.2
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.d.ts +3 -3
- package/dist/index.js +81 -38
- package/package.json +12 -3
package/dist/index.d.ts
CHANGED
|
@@ -743,7 +743,7 @@ interface PatternRule$1 {
|
|
|
743
743
|
title: string;
|
|
744
744
|
description: string;
|
|
745
745
|
suggestion: string;
|
|
746
|
-
pattern
|
|
746
|
+
pattern?: RegExp;
|
|
747
747
|
multilinePattern?: RegExp;
|
|
748
748
|
severity: Severity;
|
|
749
749
|
category: string;
|
|
@@ -769,8 +769,8 @@ interface PatternRule {
|
|
|
769
769
|
title: string;
|
|
770
770
|
description: string;
|
|
771
771
|
suggestion: string;
|
|
772
|
-
/** Applied once per line. */
|
|
773
|
-
pattern
|
|
772
|
+
/** Applied once per line. Optional — a rule may be multilinePattern-only. */
|
|
773
|
+
pattern?: RegExp;
|
|
774
774
|
/** Applied against the full file content (use sparingly). */
|
|
775
775
|
multilinePattern?: RegExp;
|
|
776
776
|
severity: Severity;
|
package/dist/index.js
CHANGED
|
@@ -2921,7 +2921,13 @@ var securityRules = [
|
|
|
2921
2921
|
title: "Potential SQL injection",
|
|
2922
2922
|
description: "SQL query built with template literals or string concatenation. User input may flow into the query unsanitised.",
|
|
2923
2923
|
suggestion: "Use parameterised queries or prepared statements instead of string interpolation.",
|
|
2924
|
-
|
|
2924
|
+
// Catches:
|
|
2925
|
+
// .query(`SELECT ... ${x}`) — template literal interpolation
|
|
2926
|
+
// .query("SELECT '" + x + "'") — double-quote outer + escaped single inside (the OWASP #1 shape)
|
|
2927
|
+
// .query('SELECT \\'" + x + "\\'') — single-quote outer + escaped double inside
|
|
2928
|
+
// .queryRawUnsafe("SELECT ... " + req.x) — Prisma raw + concat
|
|
2929
|
+
// .execute("..." + var) — plain concat
|
|
2930
|
+
pattern: /(?:query|execute|exec|raw|queryRaw|queryRawUnsafe|executeRaw|executeRawUnsafe)\s*\(\s*(?:`[^`]*\$\{|"(?:[^"\\]|\\.)*"\s*\+|'(?:[^'\\]|\\.)*'\s*\+)/i,
|
|
2925
2931
|
multilinePattern: /(?:(?:let|const|var)\s+\w+\s*=\s*["'`](?:SELECT|INSERT|UPDATE|DELETE)\b[\s\S]{1,500}?\+\s*(?:req\.|params\.|query\.|body\.|args\.|user)|(?:let|const|var)\s+\w+\s*=\s*`[^`]*(?:SELECT|INSERT|UPDATE|DELETE)\b[^`]*\$\{[^`]*`)/gim,
|
|
2926
2932
|
severity: "critical",
|
|
2927
2933
|
category: "security",
|
|
@@ -3105,7 +3111,7 @@ var securityRules = [
|
|
|
3105
3111
|
title: "Form without CSRF protection",
|
|
3106
3112
|
description: "HTML form with method=POST but no visible CSRF token field. This may be vulnerable to cross-site request forgery.",
|
|
3107
3113
|
suggestion: "Add a hidden CSRF token field to the form or use a framework-provided CSRF middleware.",
|
|
3108
|
-
|
|
3114
|
+
multilinePattern: /method\s*=\s*["']post["'][^>]*>(?![\s\S]{0,500}csrf)/i,
|
|
3109
3115
|
severity: "medium",
|
|
3110
3116
|
category: "security",
|
|
3111
3117
|
confidence: "low",
|
|
@@ -3161,7 +3167,7 @@ var securityRules = [
|
|
|
3161
3167
|
title: "XML parsing without entity restriction (XXE)",
|
|
3162
3168
|
description: "XML parsers that allow external entities can be exploited to read local files or perform SSRF.",
|
|
3163
3169
|
suggestion: "Disable external entity processing: set noent: false, or use defusedxml in Python.",
|
|
3164
|
-
|
|
3170
|
+
multilinePattern: /(?:parseXml|parseString|DOMParser|xml2js|libxmljs|XMLParser)\s*\((?![\s\S]*(?:noent:\s*false|resolve_entities:\s*false))/,
|
|
3165
3171
|
severity: "high",
|
|
3166
3172
|
category: "security",
|
|
3167
3173
|
confidence: "medium",
|
|
@@ -3211,7 +3217,7 @@ var securityRules = [
|
|
|
3211
3217
|
title: "Mass assignment via spread of user input",
|
|
3212
3218
|
description: "Spreading req.body directly into a database create/update allows attackers to set arbitrary fields (e.g. isAdmin).",
|
|
3213
3219
|
suggestion: "Explicitly pick allowed fields instead of spreading the entire request body.",
|
|
3214
|
-
|
|
3220
|
+
multilinePattern: /(?:create|update|insert|save|upsert)\s*\(\s*\{[\s\S]{0,50}\.\.\.(?:req\.body|req\.query|body|input)\b/,
|
|
3215
3221
|
severity: "high",
|
|
3216
3222
|
category: "security",
|
|
3217
3223
|
confidence: "medium",
|
|
@@ -3278,7 +3284,7 @@ var securityRules = [
|
|
|
3278
3284
|
title: "Cookie set without security flags",
|
|
3279
3285
|
description: "Cookies set without Secure, HttpOnly, or SameSite flags are vulnerable to interception and XSS theft.",
|
|
3280
3286
|
suggestion: "Set cookies with { secure: true, httpOnly: true, sameSite: 'strict' } flags.",
|
|
3281
|
-
|
|
3287
|
+
multilinePattern: /(?:res\.cookie|setCookie|set-cookie|document\.cookie)\s*(?:\(|=)(?![\s\S]{0,100}(?:secure|httpOnly|SameSite))/i,
|
|
3282
3288
|
severity: "medium",
|
|
3283
3289
|
category: "security",
|
|
3284
3290
|
confidence: "medium",
|
|
@@ -3306,7 +3312,7 @@ var securityRules = [
|
|
|
3306
3312
|
title: "CORS credentials with wildcard origin",
|
|
3307
3313
|
description: "Enabling credentials with a wildcard or reflected origin allows any site to make authenticated cross-origin requests.",
|
|
3308
3314
|
suggestion: "When using credentials: true, specify explicit trusted origins instead of '*' or reflecting the Origin header.",
|
|
3309
|
-
|
|
3315
|
+
multilinePattern: /credentials\s*:\s*true[\s\S]{0,200}origin\s*:\s*(?:["']\*["']|req\.headers\.origin|true)/,
|
|
3310
3316
|
severity: "high",
|
|
3311
3317
|
category: "security",
|
|
3312
3318
|
confidence: "high",
|
|
@@ -3343,8 +3349,10 @@ var solidityRules2 = [
|
|
|
3343
3349
|
title: "Potential reentrancy \u2014 review external call sequencing",
|
|
3344
3350
|
description: "An external call via `.call{value:...}` is present. The classic reentrancy pattern requires the call to occur BEFORE a state update \u2014 a same-line regex can't verify the order across multiple lines, so this is flagged for manual or AI review. `.send` and `.transfer` are intentionally not flagged because their 2300-gas stipend prevents reentrancy except in pathological cases (post-EIP-1884 gas changes).",
|
|
3345
3351
|
suggestion: "Follow checks-effects-interactions: update state before external calls. Or use OpenZeppelin's ReentrancyGuard. Confirm by reading the surrounding function or running AI deep-review.",
|
|
3346
|
-
|
|
3347
|
-
|
|
3352
|
+
// Only fire when .call{value:} appears AND no reentrancy-guard / OZ helper is visible
|
|
3353
|
+
// in the same file. Reduces FP storm on Governor, ERC7579, ERC4626, etc.
|
|
3354
|
+
multilinePattern: /\.call\s*\{(?:[^}]*value[^}]*)?\}\s*\([\s\S]{0,80}?\)(?![\s\S]{0,3000}?(?:nonReentrant|ReentrancyGuard|_NOT_ENTERED|_REENTRANCY_GUARD|onlyRole|onlyGovernance|_checkAuthorized|Address\.functionCall|Address\.sendValue))/,
|
|
3355
|
+
severity: "medium",
|
|
3348
3356
|
category: "security",
|
|
3349
3357
|
confidence: "low",
|
|
3350
3358
|
languages: SOL
|
|
@@ -3354,7 +3362,9 @@ var solidityRules2 = [
|
|
|
3354
3362
|
title: "tx.origin used for authorization",
|
|
3355
3363
|
description: "tx.origin returns the original sender of a transaction. Using it for auth allows phishing attacks via intermediary contracts.",
|
|
3356
3364
|
suggestion: "Use msg.sender instead of tx.origin for authorization checks.",
|
|
3357
|
-
|
|
3365
|
+
// Only flag actual auth use (tx.origin in a require/== check or returned from a getter).
|
|
3366
|
+
// Suppress comments, immutable deployer fingerprints, and Lifebuoy-style "I might add ecrecover" notes.
|
|
3367
|
+
pattern: /(?:require\s*\(\s*tx\.origin\s*==|require\s*\(\s*msg\.sender\s*==\s*tx\.origin|if\s*\(\s*tx\.origin\s*==|return\s+tx\.origin\s*;)/,
|
|
3358
3368
|
severity: "high",
|
|
3359
3369
|
category: "security",
|
|
3360
3370
|
confidence: "high",
|
|
@@ -3365,10 +3375,12 @@ var solidityRules2 = [
|
|
|
3365
3375
|
title: "Unchecked low-level call return value",
|
|
3366
3376
|
description: "Low-level call() returns a boolean success flag. Ignoring it can silently swallow failures.",
|
|
3367
3377
|
suggestion: "Always check the return value: `(bool success, ) = addr.call{...}(...); require(success);`",
|
|
3368
|
-
|
|
3369
|
-
|
|
3378
|
+
// Match low-level call result discarded (no return capture). Suppress if (bool,...) = .call() or
|
|
3379
|
+
// .call returned to a variable; suppress if onlyGovernance / onlyOwner / nonReentrant context nearby.
|
|
3380
|
+
multilinePattern: /(?<![\)\]]\s*=\s*[^=])\b\w+\.call\s*[\({][^)]{0,200}?\)\s*;(?![\s\S]{0,500}?(?:onlyGovernance|onlyRole|nonReentrant|onlyOwner))/,
|
|
3381
|
+
severity: "medium",
|
|
3370
3382
|
category: "security",
|
|
3371
|
-
confidence: "
|
|
3383
|
+
confidence: "low",
|
|
3372
3384
|
languages: SOL
|
|
3373
3385
|
},
|
|
3374
3386
|
{
|
|
@@ -3477,10 +3489,12 @@ var solidityRules2 = [
|
|
|
3477
3489
|
title: "balanceOf used for pricing without flash loan guards",
|
|
3478
3490
|
description: "Using balanceOf() for pricing calculations is vulnerable to flash loan manipulation.",
|
|
3479
3491
|
suggestion: "Use time-weighted average prices (TWAP) or Chainlink oracles instead of spot balanceOf for pricing.",
|
|
3480
|
-
|
|
3492
|
+
// Require the balanceOf call to be inside an actual function body (not a docstring/comment block),
|
|
3493
|
+
// and must NOT have TWAP/oracle/Chainlink keywords visible nearby.
|
|
3494
|
+
multilinePattern: /function\s+\w+[^{]*\{[\s\S]{0,2000}?balanceOf\s*\([^)]*\)[\s\S]{0,20}(?:\*|\/)\s*\w(?![\s\S]{0,400}?(?:TWAP|twap|Chainlink|chainlink|oracle\.|priceFeed|latestAnswer|getRoundData|observe\s*\())/,
|
|
3481
3495
|
severity: "high",
|
|
3482
3496
|
category: "security",
|
|
3483
|
-
confidence: "
|
|
3497
|
+
confidence: "low",
|
|
3484
3498
|
languages: SOL
|
|
3485
3499
|
},
|
|
3486
3500
|
{
|
|
@@ -3537,7 +3551,7 @@ var javaRules = [
|
|
|
3537
3551
|
title: "XPath injection via string concatenation",
|
|
3538
3552
|
description: "Building XPath queries with string concatenation allows injection attacks.",
|
|
3539
3553
|
suggestion: "Use XPath parameterization via XPathExpression or sanitize input.",
|
|
3540
|
-
|
|
3554
|
+
multilinePattern: /(?:XPath|xpath)[\s\S]{0,50}\.(?:evaluate|compile)\s*\(\s*(?:["'][^"']*["']\s*\+|.*\+\s*["'])/,
|
|
3541
3555
|
severity: "high",
|
|
3542
3556
|
category: "security",
|
|
3543
3557
|
confidence: "medium",
|
|
@@ -3931,7 +3945,7 @@ var performanceRules = [
|
|
|
3931
3945
|
title: "Potential N+1 query \u2014 database call inside a loop",
|
|
3932
3946
|
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
3947
|
suggestion: "Batch the queries: collect all IDs first, then execute a single WHERE IN query.",
|
|
3934
|
-
|
|
3948
|
+
multilinePattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?(?:\.find\(|\.findOne\(|\.findUnique\(|\.query\(|\.execute\(|SELECT\b)/,
|
|
3935
3949
|
severity: "high",
|
|
3936
3950
|
category: "performance",
|
|
3937
3951
|
confidence: "medium",
|
|
@@ -3942,7 +3956,7 @@ var performanceRules = [
|
|
|
3942
3956
|
title: "Nested iteration \u2014 Array search inside a loop",
|
|
3943
3957
|
description: "Using Array.find/filter/some/indexOf inside a loop is O(n*m). Use a Map or Set for O(n) lookups.",
|
|
3944
3958
|
suggestion: "Build a Map or Set from the inner array before the loop, then use .get()/.has() for O(1) lookup.",
|
|
3945
|
-
|
|
3959
|
+
multilinePattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?\.(?:find|filter|some|indexOf|includes)\s*\(/,
|
|
3946
3960
|
severity: "low",
|
|
3947
3961
|
category: "performance",
|
|
3948
3962
|
confidence: "medium",
|
|
@@ -3965,7 +3979,7 @@ var performanceRules = [
|
|
|
3965
3979
|
title: "RegExp construction inside a loop",
|
|
3966
3980
|
description: "Creating a new RegExp object on every iteration is wasteful. Regex compilation is expensive.",
|
|
3967
3981
|
suggestion: "Move the RegExp construction outside the loop and reuse the compiled pattern.",
|
|
3968
|
-
|
|
3982
|
+
multilinePattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?new\s+RegExp\s*\(/,
|
|
3969
3983
|
severity: "low",
|
|
3970
3984
|
category: "performance",
|
|
3971
3985
|
confidence: "medium",
|
|
@@ -3976,7 +3990,7 @@ var performanceRules = [
|
|
|
3976
3990
|
title: "JSON.parse inside a loop",
|
|
3977
3991
|
description: "Parsing JSON inside a loop is CPU-intensive. If the same data is re-parsed, cache the result.",
|
|
3978
3992
|
suggestion: "Parse the JSON once before the loop and reuse the result, or use streaming JSON parsing for large datasets.",
|
|
3979
|
-
|
|
3993
|
+
multilinePattern: /(?:for|while|\.forEach|\.map)\s*\([\s\S]*?JSON\.parse\s*\(/,
|
|
3980
3994
|
severity: "low",
|
|
3981
3995
|
category: "performance",
|
|
3982
3996
|
confidence: "medium",
|
|
@@ -3987,7 +4001,7 @@ var performanceRules = [
|
|
|
3987
4001
|
title: "Sequential await inside a loop",
|
|
3988
4002
|
description: "Using await inside a loop executes async operations sequentially. Independent operations can run in parallel with Promise.all.",
|
|
3989
4003
|
suggestion: "Collect promises in an array and use Promise.all() or Promise.allSettled() for parallel execution.",
|
|
3990
|
-
|
|
4004
|
+
multilinePattern: /(?:for|while)\s*\([\s\S]*?await\s+/,
|
|
3991
4005
|
severity: "low",
|
|
3992
4006
|
category: "performance",
|
|
3993
4007
|
confidence: "low",
|
|
@@ -4648,7 +4662,11 @@ var eip7702Rules = [
|
|
|
4648
4662
|
title: "EIP-7702 delegation: unguarded initializer",
|
|
4649
4663
|
description: "Contracts intended as EIP-7702 delegation targets must guard initializers \u2014 constructors don't run on delegated EOAs. An unguarded `initialize()` can be front-run, letting an attacker take ownership. The pattern also matches generic proxy initializers, which usually ARE guarded (just not by an `initializer` keyword on the same line), so confirm whether this contract is actually a 7702 target before treating as critical.",
|
|
4650
4664
|
suggestion: "If this is a 7702 delegation target: add `initializer` modifier (OpenZeppelin) or a boolean guard. For regular proxies, confirm the initializer is gated by the proxy factory.",
|
|
4651
|
-
|
|
4665
|
+
// Suppress: initialize() with explicit access guard (onlyOwner, onlyEntryPoint, noDelegateCall),
|
|
4666
|
+
// initializer modifier, ERC1967/UUPS proxy patterns, or 'once' guard.
|
|
4667
|
+
// Broader suppression: ERC-4337/7579 accounts use _setOwner pattern, OZ uses initializer modifier,
|
|
4668
|
+
// proxies use ERC1967, V4 PoolManager.initialize is permissionless-by-design with noDelegateCall.
|
|
4669
|
+
multilinePattern: /function\s+initialize\s*\([^)]*\)\s*(?:external|public)[^{;]*\{(?![\s\S]{0,800}?(?:initializer|reinitializer|_disableInitializers|require\s*\([^)]*!\s*initialized|require\s*\([^)]*owner\s*==\s*address\s*\(\s*0|noDelegateCall|onlyOwner|onlyEntryPoint|onlyProxy|ERC1967|UUPS|_setOwner|_initialize|EntryPoint|UserOperation|getStorage|StorageSlot))/,
|
|
4652
4670
|
severity: "high",
|
|
4653
4671
|
category: "solidity",
|
|
4654
4672
|
confidence: "low",
|
|
@@ -4672,10 +4690,12 @@ var tstoreRules = [
|
|
|
4672
4690
|
title: "Transient storage (TSTORE) used \u2014 verify reentrancy safety",
|
|
4673
4691
|
description: "Transient storage (EIP-1153, live since Cancun) does not impose the 2300 gas minimum of SSTORE-based guards. If this contract also uses `transfer()` or `send()` for ETH, those calls are no longer reentrancy-safe \u2014 the callee has enough gas to re-enter via TLOAD/TSTORE paths.",
|
|
4674
4692
|
suggestion: 'Audit every ETH transfer in this contract. Replace `transfer()`/`send()` with `call{value:}("")` + explicit reentrancy check. Ensure TSTORE lock is written before any external call.',
|
|
4675
|
-
|
|
4676
|
-
|
|
4693
|
+
// Only fire on ACTUAL tstore() use inside inline assembly with no nonReentrant-style guard
|
|
4694
|
+
// visible in the same file. Imports of TransientSlot alone are not a finding.
|
|
4695
|
+
multilinePattern: /assembly\s*\{[^}]{0,400}?\btstore\s*\((?![\s\S]{0,2000}?(?:nonReentrant|ReentrancyGuard|_NOT_ENTERED|_REENTRANCY_GUARD|LockBitmap|onlyByPosm|onlyByPoolManager))/,
|
|
4696
|
+
severity: "medium",
|
|
4677
4697
|
category: "solidity",
|
|
4678
|
-
confidence: "
|
|
4698
|
+
confidence: "low",
|
|
4679
4699
|
languages: SOL
|
|
4680
4700
|
},
|
|
4681
4701
|
{
|
|
@@ -4696,7 +4716,9 @@ var uniswapV4Rules = [
|
|
|
4696
4716
|
title: "Uniswap v4 hook: missing PoolManager sender validation",
|
|
4697
4717
|
description: "Hook callbacks (`beforeSwap`, `afterSwap`, `beforeAddLiquidity`, etc.) must verify `msg.sender == address(poolManager)`. Without this check, any address can call the hook directly and manipulate its state.",
|
|
4698
4718
|
suggestion: 'Add `require(msg.sender == address(poolManager), "not PoolManager");` as the first line of every hook callback, or inherit from `BaseHook` which enforces this automatically.',
|
|
4699
|
-
|
|
4719
|
+
// Require function BODY (not interface signature ending in ;) AND missing sender check.
|
|
4720
|
+
// Skip interface files and abstract declarations.
|
|
4721
|
+
multilinePattern: /function\s+(?:before|after)(?:Swap|AddLiquidity|RemoveLiquidity|Initialize|Donate)\s*\([^)]*\)\s*(?:external|public)[^{;]*\{(?![\s\S]{0,300}?(?:msg\.sender\s*==\s*(?:address\s*\()?\s*(?:poolManager|manager|_poolManager|POOL_MANAGER|i_poolManager)|onlyByPoolManager|onlyByManager|onlyPoolManager|BaseHook|_validatePoolManager))/,
|
|
4700
4722
|
severity: "critical",
|
|
4701
4723
|
category: "solidity",
|
|
4702
4724
|
confidence: "medium",
|
|
@@ -4719,7 +4741,9 @@ var uniswapV4Rules = [
|
|
|
4719
4741
|
title: "Uniswap v4 hook: BalanceDelta returned without settle/take",
|
|
4720
4742
|
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
4743
|
suggestion: "Ensure `poolManager.settle()` or `poolManager.take()` is called for both currency0 and currency1 before returning from the unlock callback.",
|
|
4722
|
-
|
|
4744
|
+
// Only fire on hook callbacks specifically (before/after-Swap etc.) — not interface decls,
|
|
4745
|
+
// not PoolManager itself (which is the singleton, returns deltas BY DESIGN).
|
|
4746
|
+
multilinePattern: /function\s+(?:before|after)(?:Swap|AddLiquidity|RemoveLiquidity|Donate)\s*\([^)]*\)\s*(?:external|public)[^{;]*returns\s*\([^)]*BalanceDelta[^)]*\)[^{;]*\{(?![\s\S]{0,800}?(?:settle|\.take\s*\(|poolManager\.))/,
|
|
4723
4747
|
severity: "high",
|
|
4724
4748
|
category: "solidity",
|
|
4725
4749
|
confidence: "low",
|
|
@@ -4730,7 +4754,7 @@ var uniswapV4Rules = [
|
|
|
4730
4754
|
title: "Uniswap v4 hook: overly broad hook permissions",
|
|
4731
4755
|
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
4756
|
suggestion: "Return only the minimum required `Hooks.Permissions` from `getHookPermissions()`. Audit each permission flag against your actual callback implementations.",
|
|
4733
|
-
|
|
4757
|
+
multilinePattern: /getHookPermissions\s*\(\s*\)\s*(?:public|external|override)[^{]*\{[\s\S]{0,300}?true/,
|
|
4734
4758
|
severity: "medium",
|
|
4735
4759
|
category: "solidity",
|
|
4736
4760
|
confidence: "low",
|
|
@@ -4741,7 +4765,9 @@ var uniswapV4Rules = [
|
|
|
4741
4765
|
title: "Uniswap v4 hook: spot sqrtPriceX96 used for pricing without limit",
|
|
4742
4766
|
description: "Using spot `sqrtPriceX96` read from pool state inside a hook callback for pricing decisions without a `sqrtPriceLimitX96` bound is sandwich-attackable. The price reflects post-swap state which may be manipulated.",
|
|
4743
4767
|
suggestion: "Pass `sqrtPriceLimitX96` to all swap calls from hooks. Use a TWAP oracle for pricing rather than spot price.",
|
|
4744
|
-
|
|
4768
|
+
// Only fire inside hook callback functions (before/after Swap/AddLiquidity), not library files
|
|
4769
|
+
// (Pool.sol, TickMath.sol, etc.) which legitimately work with raw sqrt prices.
|
|
4770
|
+
multilinePattern: /function\s+(?:before|after)(?:Swap|AddLiquidity|RemoveLiquidity|Donate)\s*\([^)]*\)\s*(?:external|public)[^{;]*\{[\s\S]{0,1500}?sqrtPriceX96\s*[*/+\-]\s*\w(?![\s\S]{0,300}?(?:TWAP|twap|sqrtPriceLimitX96|observe\s*\())/,
|
|
4745
4771
|
severity: "high",
|
|
4746
4772
|
category: "solidity",
|
|
4747
4773
|
confidence: "low",
|
|
@@ -4773,7 +4799,9 @@ var hackReplayRules = [
|
|
|
4773
4799
|
title: "Euler hack pattern \u2014 donate function reachable from liquidation path",
|
|
4774
4800
|
description: "A donate/donateTo* function in a lending or vault context can be weaponized to artificially worsen the caller's health factor, then trigger self-liquidation at a profit. This is the exact vector that drained $197M from Euler Finance in March 2023.",
|
|
4775
4801
|
suggestion: "Either remove the donate function, or (a) require donations to come from accounts with no active borrows, (b) recompute health AFTER the donation as if the donated assets remained the donor's, and (c) prevent same-block liquidation of the donor.",
|
|
4776
|
-
|
|
4802
|
+
// Only flag donate functions in lending/health/liquidation contexts. Skip pure liquidity-pool
|
|
4803
|
+
// donate functions (Uniswap V4 PoolManager.donate gives fees to LPs — different semantics).
|
|
4804
|
+
multilinePattern: /function\s+donate[A-Za-z]*\s*\([^)]*\)[^{;]*\{[\s\S]{0,2000}?(?:healthFactor|accountHealth|isHealthy|borrow|collateral|liquidat|debt|vault|reserves)/,
|
|
4777
4805
|
severity: "high",
|
|
4778
4806
|
category: "hack-replay",
|
|
4779
4807
|
confidence: "medium",
|
|
@@ -4789,7 +4817,7 @@ var hackReplayRules = [
|
|
|
4789
4817
|
title: "Radiant hack pattern \u2014 single-step ownership transfer (no Ownable2Step)",
|
|
4790
4818
|
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
4819
|
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
|
-
|
|
4820
|
+
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
4821
|
severity: "high",
|
|
4794
4822
|
category: "hack-replay",
|
|
4795
4823
|
confidence: "medium",
|
|
@@ -4841,7 +4869,13 @@ var hackReplayRules = [
|
|
|
4841
4869
|
title: "Beanstalk hack pattern \u2014 governance execute() with no timelock between vote and call",
|
|
4842
4870
|
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
4871
|
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
|
-
|
|
4872
|
+
// Expanded negative lookahead to suppress OZ Governor/AccessManager/AccessControl
|
|
4873
|
+
// patterns that use proper auth/delay even if "timelock" keyword isn't right next to execute().
|
|
4874
|
+
// Suppress: ERC-4337/7579 account abstraction execute(mode, data), OZ Governor, AccessManager,
|
|
4875
|
+
// anything with auth modifiers. Real Beanstalk-shape is execute(proposalId) on a governance contract.
|
|
4876
|
+
// Suppress: interface signatures (ending in ;), OZ Governor/IGovernor, AccessManager, account
|
|
4877
|
+
// abstraction execute() patterns, anything with proper auth. Requires actual function BODY.
|
|
4878
|
+
multilinePattern: /function\s+(?:execute(?:Proposal)?|propose(?:AndExecute)?)\s*\([^)]*\)\s*(?:external|public)(?:\s+payable)?(?:\s+returns)?[^{;]*\{(?![\s\S]{0,1500}?(?:timelock|Timelock|TimelockController|queued|block\.timestamp\s*[><]=?\s*\w+\s*\+|onlyRole|onlyGovernance|onlyEntryPoint|onlyEntryPointOrSelf|_checkAuthorized|_authorizeUpgrade|AccessControl|AccessManager|Governor|IGovernor|hasRole|getMinDelay|delay\s*\(|_executeOperations|hashOperation|isOperationReady|EntryPoint|ERC7579|ERC4337|ERC7821|IAccount|_validateUserOp|UserOperation|state\s*\(\s*proposalId|_castVote|votingDelay))/,
|
|
4845
4879
|
severity: "critical",
|
|
4846
4880
|
category: "hack-replay",
|
|
4847
4881
|
confidence: "low",
|
|
@@ -4857,7 +4891,7 @@ var hackReplayRules = [
|
|
|
4857
4891
|
title: "Multichain hack pattern \u2014 bridge withdraw/unlock gated by single owner role",
|
|
4858
4892
|
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
4893
|
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
|
-
|
|
4894
|
+
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
4895
|
severity: "critical",
|
|
4862
4896
|
category: "hack-replay",
|
|
4863
4897
|
confidence: "medium",
|
|
@@ -4909,8 +4943,11 @@ var hackReplayRules = [
|
|
|
4909
4943
|
title: "Wormhole hack pattern \u2014 guardian/validator signature accepted without strict set verification",
|
|
4910
4944
|
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
4945
|
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
|
-
|
|
4913
|
-
|
|
4946
|
+
// Suppress legitimate EIP-712 / Permit / typed-data signature flows (OZ ECDSA, EIP-2612 permits,
|
|
4947
|
+
// ERC-4337 user-op validation) which use ecrecover correctly. Only fire on bridge/oracle-shaped
|
|
4948
|
+
// contexts where signer-set verification matters.
|
|
4949
|
+
multilinePattern: /(?:ecrecover|ECDSA\.recover|\.recover)\s*\([\s\S]{0,400}?\)(?![\s\S]{0,800}?(?:guardianSet|validatorSet|currentSetHash|setHash|_hashTypedDataV4|EIP712|DOMAIN_SEPARATOR|PERMIT_TYPEHASH|DELEGATION_TYPEHASH|ERC1271|isValidSignature|nonces\s*\[|require\s*\([^)]*==\s*expected))/,
|
|
4950
|
+
severity: "high",
|
|
4914
4951
|
category: "hack-replay",
|
|
4915
4952
|
confidence: "low",
|
|
4916
4953
|
languages: SOL
|
|
@@ -4945,7 +4982,7 @@ var hackReplayRules = [
|
|
|
4945
4982
|
// Tightened: require the call to appear in a function whose name implies
|
|
4946
4983
|
// lending / collateral / liquidation context (the actual hack surface).
|
|
4947
4984
|
// Plain DEX pool internals like Uniswap getReserves are not at risk.
|
|
4948
|
-
|
|
4985
|
+
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
4986
|
severity: "medium",
|
|
4950
4987
|
category: "hack-replay",
|
|
4951
4988
|
confidence: "low",
|
|
@@ -5035,7 +5072,9 @@ var rugSurfaceRules = [
|
|
|
5035
5072
|
title: "Role admin can grant itself any role",
|
|
5036
5073
|
description: "DEFAULT_ADMIN_ROLE or a self-managed role can call grantRole() on itself or on other privileged roles. Single admin compromise = full takeover.",
|
|
5037
5074
|
suggestion: "Use AccessControlEnumerable + revoke admin's ability to manage its own role. Move sensitive roles to a separate multisig.",
|
|
5038
|
-
|
|
5075
|
+
// Only flag PUBLIC/EXTERNAL grantRole(role, msg.sender) or non-constructor _grantRole.
|
|
5076
|
+
// OZ AccessControl's constructor _grantRole(DEFAULT_ADMIN_ROLE, initialAdmin) is legit setup.
|
|
5077
|
+
multilinePattern: /function\s+\w+\s*\([^)]*\)\s*(?:external|public)[^{]*\{[\s\S]{0,400}?(?:_setupRole\s*\(\s*DEFAULT_ADMIN_ROLE|_grantRole\s*\(\s*DEFAULT_ADMIN_ROLE|grantRole\s*\([^,]+,\s*msg\.sender\s*\))/,
|
|
5039
5078
|
severity: "high",
|
|
5040
5079
|
category: "rug-surface",
|
|
5041
5080
|
confidence: "medium",
|
|
@@ -5046,7 +5085,7 @@ var rugSurfaceRules = [
|
|
|
5046
5085
|
title: "Renounceable ownership without evidence of renunciation",
|
|
5047
5086
|
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
5087
|
suggestion: "Either renounce ownership in the constructor (acceptably for fully-immutable contracts), or transfer to a multisig. Document this explicitly.",
|
|
5049
|
-
|
|
5088
|
+
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
5089
|
severity: "medium",
|
|
5051
5090
|
category: "rug-surface",
|
|
5052
5091
|
confidence: "low",
|
|
@@ -5068,7 +5107,7 @@ var rugSurfaceRules = [
|
|
|
5068
5107
|
title: "Privileged modifier without visible role check in body",
|
|
5069
5108
|
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
5109
|
suggestion: "Rename modifier to make privilege explicit (e.g. `onlyOperator`). Or move check inline so reviewers see it in the function body.",
|
|
5071
|
-
|
|
5110
|
+
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
5111
|
severity: "medium",
|
|
5073
5112
|
category: "rug-surface",
|
|
5074
5113
|
confidence: "low",
|
|
@@ -5395,6 +5434,7 @@ function scanFile(relPath, content, rules, changedRanges) {
|
|
|
5395
5434
|
const applicableRules = rules.filter((r) => ruleAppliesToFile(r, relPath));
|
|
5396
5435
|
if (applicableRules.length === 0) return findings;
|
|
5397
5436
|
for (const rule of applicableRules) {
|
|
5437
|
+
if (!rule.pattern) continue;
|
|
5398
5438
|
for (let i = 0; i < lines.length; i++) {
|
|
5399
5439
|
const lineNumber = i + 1;
|
|
5400
5440
|
const line = lines[i];
|
|
@@ -5426,8 +5466,10 @@ function scanFile(relPath, content, rules, changedRanges) {
|
|
|
5426
5466
|
if (rule.id === "cp-clean-callback-hell" && isTestFile(relPath)) continue;
|
|
5427
5467
|
if (rule.id === "cp-sec-command-injection" && isScriptDir(relPath)) continue;
|
|
5428
5468
|
rule.multilinePattern.lastIndex = 0;
|
|
5469
|
+
const isGlobal = rule.multilinePattern.flags.includes("g");
|
|
5429
5470
|
let match;
|
|
5430
5471
|
while ((match = rule.multilinePattern.exec(content)) !== null) {
|
|
5472
|
+
const breakAfter = !isGlobal;
|
|
5431
5473
|
const textBefore = content.slice(0, match.index);
|
|
5432
5474
|
const startLine = textBefore.split("\n").length;
|
|
5433
5475
|
const matchLines = match[0].split("\n").length;
|
|
@@ -5453,6 +5495,7 @@ function scanFile(relPath, content, rules, changedRanges) {
|
|
|
5453
5495
|
if (match[0].length === 0) {
|
|
5454
5496
|
rule.multilinePattern.lastIndex++;
|
|
5455
5497
|
}
|
|
5498
|
+
if (breakAfter) break;
|
|
5456
5499
|
}
|
|
5457
5500
|
}
|
|
5458
5501
|
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.2",
|
|
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
|
},
|