@elytrasec/engine 0.4.2 → 0.4.4

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 +61 -10
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3028,12 +3028,48 @@ var securityRules = [
3028
3028
  title: "Potential command injection",
3029
3029
  description: "Shell command built with string concatenation or template literals. Attacker-controlled input may escape the intended command.",
3030
3030
  suggestion: "Use execFile() with an argument array or a library like execa that avoids shell interpretation.",
3031
- pattern: /(?:exec|execSync|spawn|spawnSync|system)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+)/,
3031
+ // Catches:
3032
+ // JS: exec(`ls ${dir}`), execSync("cmd " + x), spawn("...", ...)
3033
+ // Py: os.system(f"ping {host}"), os.popen("cmd " + x), subprocess.call("..." + x, shell=True)
3034
+ pattern: /(?:exec|execSync|spawn|spawnSync|system|popen|run|call|check_output|getoutput)\s*\(\s*(?:`[^`]*\$\{|[fF]"(?:[^"\\]|\\.)*\{|[fF]'(?:[^'\\]|\\.)*\{|"(?:[^"\\]|\\.)*"\s*\+|'(?:[^'\\]|\\.)*'\s*\+)/,
3032
3035
  severity: "critical",
3033
3036
  category: "security",
3034
3037
  confidence: "medium",
3035
3038
  languages: [...JS_TS, ...PY]
3036
3039
  },
3040
+ {
3041
+ id: "cp-sec-python-eval",
3042
+ title: "Python eval() / exec() with dynamic input",
3043
+ description: "Python `eval()` or `exec()` executes arbitrary code from a string. Reachable via `request.args`, `request.form`, or any user-controlled variable, this is direct RCE.",
3044
+ suggestion: "Avoid eval/exec entirely. For arithmetic, use `ast.literal_eval`. For dispatch, use an explicit allow-list dict.",
3045
+ pattern: /\b(?:eval|exec)\s*\(\s*(?:request\.|input\s*\(|sys\.argv|os\.environ|[\w.]+\.(?:args|form|query|params|json|body))/,
3046
+ severity: "critical",
3047
+ category: "security",
3048
+ confidence: "high",
3049
+ languages: PY
3050
+ },
3051
+ {
3052
+ id: "cp-sec-python-eval-loose",
3053
+ title: "Python eval() / exec() call",
3054
+ description: "Python's `eval()` and `exec()` evaluate arbitrary code. Even when input looks trusted, these are common code-injection vectors.",
3055
+ suggestion: "Replace with `ast.literal_eval` (for data literals) or an explicit allow-list. If you must use eval, scope the globals/locals dict to empty.",
3056
+ pattern: /\b(?:eval|exec)\s*\(\s*(?!{|\[|'\)|"\)|None|globals|locals)/,
3057
+ severity: "high",
3058
+ category: "security",
3059
+ confidence: "low",
3060
+ languages: PY
3061
+ },
3062
+ {
3063
+ id: "cp-sec-go-path-traversal",
3064
+ title: "Go file read with unsanitized user input (path traversal)",
3065
+ description: "`ioutil.ReadFile` / `os.ReadFile` / `os.Open` constructed via concatenation with HTTP request data is path-traversal-vulnerable. Attackers pass `../../etc/passwd`.",
3066
+ suggestion: "Use `filepath.Clean` and verify the resolved path starts with the expected base directory. Better: maintain an allow-list of accepted file IDs.",
3067
+ pattern: /(?:ioutil\.ReadFile|os\.ReadFile|os\.Open(?:File)?)\s*\([^)]*(?:\+\s*\w+\s*\)|r\.URL\.Query|r\.FormValue|mux\.Vars)/,
3068
+ severity: "high",
3069
+ category: "security",
3070
+ confidence: "medium",
3071
+ languages: [".go"]
3072
+ },
3037
3073
  {
3038
3074
  id: "cp-sec-open-redirect",
3039
3075
  title: "Potential open redirect",
@@ -3477,8 +3513,11 @@ var solidityRules2 = [
3477
3513
  title: "Chainlink latestRoundData without staleness check",
3478
3514
  description: "Using latestRoundData() without checking updatedAt for staleness can return outdated prices.",
3479
3515
  suggestion: "Check that `updatedAt` is recent: `require(block.timestamp - updatedAt < STALENESS_THRESHOLD)`.",
3480
- pattern: /latestRoundData\s*\(/,
3481
- multilinePattern: /latestRoundData\s*\([\s\S]{0,500}(?!updatedAt)/g,
3516
+ // Removed single-line pattern: it fired on EVERY interface declaration of latestRoundData().
3517
+ // Only the multilinePattern below should fire — it scopes to actual usage with no staleness check.
3518
+ // Match real usage (destructured assignment), suppress when staleness/sequencer/heartbeat
3519
+ // checks are visible in the next ~1500 chars. Skip pure interface declarations.
3520
+ multilinePattern: /\(\s*[^)]*\)\s*=\s*[^;]*\.latestRoundData\s*\(\s*\)(?![\s\S]{0,1500}?(?:updatedAt|staleness|MAX_DELAY|STALENESS|grace|GRACE|timestamp\s*-\s*updatedAt|block\.timestamp\s*-\s*\w+|sequencer|isSequencerUp|MAX_HEARTBEAT|HEARTBEAT|maxAge))/,
3482
3521
  severity: "high",
3483
3522
  category: "security",
3484
3523
  confidence: "medium",
@@ -4666,7 +4705,11 @@ var eip7702Rules = [
4666
4705
  // initializer modifier, ERC1967/UUPS proxy patterns, or 'once' guard.
4667
4706
  // Broader suppression: ERC-4337/7579 accounts use _setOwner pattern, OZ uses initializer modifier,
4668
4707
  // 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))/,
4708
+ // Suppress when function declaration line itself contains a guard modifier
4709
+ // (initializer, onlyOwner, onlyProxy, noDelegateCall, payable initializer, etc.) — these
4710
+ // come BEFORE the opening { so the prior lookahead-after-brace missed them.
4711
+ // Then suppress when proxy/4337/UUPS/admin patterns appear in body.
4712
+ multilinePattern: /function\s+initialize\s*\([^)]*\)\s*(?:external|public)\s+(?!.*(?:initializer|reinitializer|onlyOwner|onlyProxy|onlyEntryPoint|noDelegateCall|onlyRole))[^{;]*\{(?![\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|_setImplementation|_changeAdmin|EntryPoint|UserOperation|getStorage|StorageSlot|IMPLEMENTATION_SLOT|ADMIN_SLOT|require\s*\([^)]*==\s*address\s*\(\s*0\s*\)|_logic\s*\.\s*delegatecall))/,
4670
4713
  severity: "high",
4671
4714
  category: "solidity",
4672
4715
  confidence: "low",
@@ -4836,8 +4879,12 @@ var hackReplayRules = [
4836
4879
  title: "zkSync hack pattern \u2014 admin-only sweep/drain/rescue function",
4837
4880
  description: "An admin-gated function with a name like sweep*, drain*, rescue*, emergencyWithdraw*, forceMint*, adminMint* is the exact category of function the zkSync airdrop attacker exploited (April 2025, $5M loss). Even with a timelock check inside, these functions are high-value targets \u2014 a compromised admin key bypasses everything. The pattern is reviewed manually because a same-line regex cannot verify whether a timelock guard is enforced.",
4838
4881
  suggestion: "Audit this function carefully: (a) is it gated by a TimelockController contract address check, not just an onlyOwner modifier? (b) is the action bounded (max amount per call, per epoch)? (c) is there an N-of-M multisig requirement at the modifier level? If any answer is no, add it before mainnet.",
4839
- pattern: /function\s+(?:sweep[A-Za-z]*|drain[A-Za-z]*|rescue[A-Za-z]*|emergencyWithdraw[A-Za-z]*|forceMint[A-Za-z]*|adminMint[A-Za-z]*)\s*\([^)]*\)\s*(?:external|public)/,
4840
- severity: "high",
4882
+ // Suppress when function has a multi-element lock (onlyRescuer with named slot),
4883
+ // onlyPoolAdmin pattern (Aave), or other named-role modifiers that imply auditing-already-done.
4884
+ // Real catch is "onlyOwner" plain — anonymous single-role drain.
4885
+ multilinePattern: /function\s+(?:sweep[A-Za-z]*|drain[A-Za-z]*|rescue[A-Za-z]*|emergencyWithdraw[A-Za-z]*|forceMint[A-Za-z]*|adminMint[A-Za-z]*)\s*\([^)]*\)\s*(?:external|public)(?!.*(?:onlyRescuer|onlyPoolAdmin|onlyEmergencyAdmin|onlyRiskAdmin|onlyAclAdmin|onlyRoleAdmin|onlyConfigurator|hasRole))[^{;]*\{/,
4886
+ // Downgraded from high to medium: this is a "review needed" finding, not "definitely vulnerable"
4887
+ severity: "medium",
4841
4888
  category: "hack-replay",
4842
4889
  confidence: "medium",
4843
4890
  languages: SOL
@@ -4873,9 +4920,8 @@ var hackReplayRules = [
4873
4920
  // patterns that use proper auth/delay even if "timelock" keyword isn't right next to execute().
4874
4921
  // Suppress: ERC-4337/7579 account abstraction execute(mode, data), OZ Governor, AccessManager,
4875
4922
  // 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))/,
4923
+ // Expanded suppression: ERC-2771 meta-tx forwarders, ERC-6551 token-bound accounts.
4924
+ 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|ERC2771|ERC6551|Forwarder|ForwardRequest|MetaTx|TokenBoundAccount|_verify|IAccount|_validateUserOp|UserOperation|state\s*\(\s*proposalId|_castVote|votingDelay))/,
4879
4925
  severity: "critical",
4880
4926
  category: "hack-replay",
4881
4927
  confidence: "low",
@@ -4946,7 +4992,11 @@ var hackReplayRules = [
4946
4992
  // Suppress legitimate EIP-712 / Permit / typed-data signature flows (OZ ECDSA, EIP-2612 permits,
4947
4993
  // ERC-4337 user-op validation) which use ecrecover correctly. Only fire on bridge/oracle-shaped
4948
4994
  // 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))/,
4995
+ // EIP-712 / permit / typed-data patterns can appear BEFORE or AFTER ecrecover. Use a
4996
+ // wrapping check: require the recover call to be in a CONTEXT that contains typed-data
4997
+ // keywords within ~1000 chars on either side. We approximate by lookahead-only with a
4998
+ // bigger window AND lookbehind-by-context (check if file declares typed-data structures).
4999
+ multilinePattern: /(?:ecrecover|ECDSA\.recover|\w+\.recover)\s*\([\s\S]{0,400}?\)(?![\s\S]{0,1500}?(?:guardianSet|validatorSet|currentSetHash|setHash|_hashTypedDataV4|EIP712|EIP_712|DOMAIN_SEPARATOR|PERMIT_TYPEHASH|DELEGATION_TYPEHASH|ERC1271|isValidSignature|_?nonces\s*\[|_approve|_approveDelegation|_useCheckedNonce|_useNonce|require\s*\([^)]*==\s*expected))/,
4950
5000
  severity: "high",
4951
5001
  category: "hack-replay",
4952
5002
  confidence: "low",
@@ -5465,6 +5515,7 @@ function scanFile(relPath, content, rules, changedRanges) {
5465
5515
  if (!rule.multilinePattern) continue;
5466
5516
  if (rule.id === "cp-clean-callback-hell" && isTestFile(relPath)) continue;
5467
5517
  if (rule.id === "cp-sec-command-injection" && isScriptDir(relPath)) continue;
5518
+ if (rule.id === "cp-hack-wormhole-unchecked-signature-set" && /\b(?:EIP712|DOMAIN_SEPARATOR|_hashTypedDataV4|PERMIT_TYPEHASH|DELEGATION_TYPEHASH|ERC1271|EIP712Upgradeable)\b/.test(content)) continue;
5468
5519
  rule.multilinePattern.lastIndex = 0;
5469
5520
  const isGlobal = rule.multilinePattern.flags.includes("g");
5470
5521
  let match;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elytrasec/engine",
3
- "version": "0.4.2",
3
+ "version": "0.4.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.",
5
5
  "license": "MIT",
6
6
  "author": "ElytraSec <hello@elytrasec.io>",