diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index c346668..bd7e3ff 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -50,7 +50,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5
+ uses: github/codeql-action/init@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -63,7 +63,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5
+ uses: github/codeql-action/autobuild@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -76,6 +76,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5
+ uses: github/codeql-action/analyze@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8
with:
category: "/language:${{matrix.language}}"
\ No newline at end of file
diff --git a/.github/workflows/estree-ast-utils.yml b/.github/workflows/estree-ast-utils.yml
new file mode 100644
index 0000000..be460d3
--- /dev/null
+++ b/.github/workflows/estree-ast-utils.yml
@@ -0,0 +1,41 @@
+# 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
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
+
+name: Node.js CI
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - workspaces/estree-ast-utils/**
+ pull_request:
+ paths:
+ - workspaces/estree-ast-utils/**
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ node-version: [18.x, 20.x]
+ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
+
+ steps:
+ - name: Harden Runner
+ uses: step-security/harden-runner@eb238b55efaa70779f274895e782ed17c84f2895 # v2.6.1
+ with:
+ egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
+
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
+ with:
+ node-version: ${{ matrix.node-version }}
+ - run: npm install
+ - run: npm test
diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index 245317a..8457575 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -31,7 +31,7 @@ jobs:
- name: Run tests
run: npm run test
- name: Send coverage report to Codecov
- uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
+ uses: codecov/codecov-action@428cda1b1c731be3e8bfa389049c3f276d572ffb # v4.0.0-beta.3
nsci:
runs-on: ubuntu-latest
strategy:
@@ -51,7 +51,7 @@ jobs:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm install
- - uses: NodeSecure/ci-action@e3ac9c03585752e979622279106a161e94d5717b # v1
+ - uses: NodeSecure/ci-action@177c57fe32c75cafabe87f6e4515d277cc37ae6c # v1.4.1
with:
warnings: warning
vulnerabilities: off
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
index 682f0cd..2e35ffa 100644
--- a/.github/workflows/scorecard.yml
+++ b/.github/workflows/scorecard.yml
@@ -37,12 +37,12 @@ jobs:
egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs
- name: "Checkout code"
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v3.3.0 # v3.1.0
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
persist-credentials: false
- name: "Run analysis"
- uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # v2.2.0
+ uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
with:
results_file: results.sarif
results_format: sarif
@@ -64,7 +64,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
- uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
+ uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: SARIF file
path: results.sarif
@@ -72,6 +72,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
- uses: github/codeql-action/upload-sarif@00e563ead9f72a8461b24876bee2d0c2e8bd2ee8 # v2.21.5
+ uses: github/codeql-action/upload-sarif@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8
with:
sarif_file: results.sarif
diff --git a/.github/workflows/sec-literal.yml b/.github/workflows/sec-literal.yml
new file mode 100644
index 0000000..43ddff1
--- /dev/null
+++ b/.github/workflows/sec-literal.yml
@@ -0,0 +1,29 @@
+name: Node.js CI
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - workspaces/sec-literal/**
+ pull_request:
+ paths:
+ - workspaces/sec-literal/**
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ node-version: [18.x, 20.x]
+ fail-fast: false
+ steps:
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0
+ with:
+ node-version: ${{ matrix.node-version }}
+ - name: Install dependencies
+ run: npm install
+ - name: Run tests
+ run: npm run test
diff --git a/README.md b/README.md
index 873d51c..d107655 100644
--- a/README.md
+++ b/README.md
@@ -194,6 +194,21 @@ export type ReportOnFile = {
+## Workspaces
+
+Click on one of the links to access the documentation of the workspace:
+
+| name | package and link |
+| --- | --- |
+| estree-ast-util | [@nodesecure/estree-ast-util](./workspaces/estree-ast-util) |
+| sec-literal | [@nodesecure/sec-literal ](./workspaces/sec-literal) |
+
+These packages are 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).
+```bash
+$ npm i @nodesecure/estree-ast-util
+# or
+$ yarn add @nodesecure/estree-ast-util
+```
## Contributors ✨
diff --git a/package.json b/package.json
index 78955f7..5f3cfa7 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,10 @@
"type": "git",
"url": "git+https://github.com/NodeSecure/js-x-ray.git"
},
+ "workspaces": [
+ "workspaces/estree-ast-utils",
+ "workspaces/sec-literal"
+ ],
"keywords": [
"ast",
"nsecure",
@@ -48,10 +52,15 @@
},
"devDependencies": {
"@nodesecure/eslint-config": "^1.6.0",
+ "@small-tech/esm-tape-runner": "^2.0.0",
+ "@small-tech/tap-monkey": "^1.4.0",
"@types/node": "^20.6.2",
"c8": "^8.0.1",
+ "cross-env": "^7.0.3",
"eslint": "^8.31.0",
"glob": "^10.3.4",
- "pkg-ok": "^3.0.0"
+ "iterator-matcher": "^2.1.0",
+ "pkg-ok": "^3.0.0",
+ "tape": "^5.7.2"
}
}
diff --git a/workspaces/estree-ast-utils/LICENSE b/workspaces/estree-ast-utils/LICENSE
new file mode 100644
index 0000000..346097d
--- /dev/null
+++ b/workspaces/estree-ast-utils/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 NodeSecure
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/workspaces/estree-ast-utils/README.md b/workspaces/estree-ast-utils/README.md
new file mode 100644
index 0000000..27a3b14
--- /dev/null
+++ b/workspaces/estree-ast-utils/README.md
@@ -0,0 +1,109 @@
+# estree-ast-utils
+
+[![version](https://img.shields.io/badge/dynamic/json.svg?style=for-the-badge&url=https://raw.githubusercontent.com/NodeSecure/js-x-ray/blob/master/workspaces/estree-ast-utils/main/package.json&query=$.version&label=Version)](https://www.npmjs.com/package/@NodeSecure/js-x-ray/blob/master/workspaces/estree-ast-utils)
+[![maintained](https://img.shields.io/badge/Maintained%3F-yes-green.svg?style=for-the-badge)](https://github.com/NodeSecure/js-x-ray/blob/master/workspaces/estree-ast-utils/graphs/commit-activity)
+[![OpenSSF
+Scorecard](https://api.securityscorecards.dev/projects/github.com/NodeSecure/js-x-ray/blob/master/workspaces/estree-ast-utils/badge?style=for-the-badge)](https://api.securityscorecards.dev/projects/github.com/NodeSecure/js-x-ray/blob/master/workspaces/estree-ast-utils)
+[![mit](https://img.shields.io/github/license/NodeSecure/js-x-ray/blob/master/workspaces/estree-ast-utils.svg?style=for-the-badge)](https://github.com/NodeSecure/js-x-ray/blob/master/workspaces/estree-ast-utils/blob/main/LICENSE)
+[![build](https://img.shields.io/github/actions/workflow/status/NodeSecure/js-x-ray/blob/master/workspaces/estree-ast-utils/node.js.yml?style=for-the-badge)](https://github.com/NodeSecure/js-x-ray/blob/master/workspaces/estree-ast-utils/actions?query=workflow%3A%22Node.js+CI%22)
+
+Utilities for AST (ESTree compliant)
+
+## Getting Started
+
+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).
+
+```bash
+$ npm i @nodesecure/estree-ast-utils
+# or
+$ yarn add @nodesecure/estree-ast-utils
+```
+
+## Usage example
+
+```js
+import { VariableTracer } from "@nodesecure/estree-ast-utils";
+
+const tracer = new VariableTracer().enableDefaultTracing();
+
+const data = tracer.getDataFromIdentifier("identifier...here");
+console.log(data);
+```
+
+## API
+
+arrayExpressionToString(node): IterableIterator< string >
+
+Translate an ESTree ArrayExpression into an iterable of Literal value.
+
+```js
+["foo", "bar"];
+```
+
+will return `"foo"` then `"bar"`.
+
+
+
+concatBinaryExpression(node, options): IterableIterator< string >
+
+Return all Literal part of a given Binary Expression.
+
+```js
+"foo" + "bar";
+```
+
+will return `"foo"` then `"bar"`.
+
+One of the options of the method is `stopOnUnsupportedNode`, if true it will throw an Error if the left or right side of the Expr is not a supported type.
+
+
+
+getCallExpressionIdentifier(node): string | null
+
+Return the identifier name of the CallExpression (or null if there is none).
+
+```js
+foobar();
+```
+
+will return `"foobar"`.
+
+
+
+getMemberExpressionIdentifier(node): IterableIterator< string >
+
+Return the identifier name of the CallExpression (or null if there is none).
+
+```js
+foo.bar();
+```
+
+will return `"foo"` then `"bar"`.
+
+
+
+getVariableDeclarationIdentifiers(node): IterableIterator< string >
+
+Get all variables identifier name.
+
+```js
+const [foo, bar] = [1, 2];
+```
+
+will return `"foo"` then `"bar"`.
+
+
+
+isLiteralRegex(node): boolean
+
+Return `true` if the given Node is a Literal Regex Node.
+
+```js
+/^hello/g;
+```
+
+
+
+## License
+
+MIT
diff --git a/workspaces/estree-ast-utils/package.json b/workspaces/estree-ast-utils/package.json
new file mode 100644
index 0000000..8afe1d1
--- /dev/null
+++ b/workspaces/estree-ast-utils/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@nodesecure/estree-ast-utils",
+ "version": "1.4.1",
+ "description": "Utilities for AST (ESTree compliant)",
+ "type": "module",
+ "exports": "./src/index.js",
+ "scripts": {
+ "lint": "eslint src test",
+ "prepublishOnly": "pkg-ok",
+ "test": "cross-env esm-tape-runner 'test/**/*.spec.js' | tap-monkey",
+ "check": "cross-env npm run lint && npm run test",
+ "coverage": "c8 -r html npm test"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/NodeSecure/estree-ast-utils.git"
+ },
+ "keywords": [
+ "estree",
+ "ast",
+ "utils"
+ ],
+ "author": "GENTILHOMME Thomas ",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/NodeSecure/estree-ast-utils/issues"
+ },
+ "homepage": "https://github.com/NodeSecure/estree-ast-utils#readme",
+ "devDependencies": {
+ "estree-walker": "^3.0.2"
+ },
+ "dependencies": {
+ "@nodesecure/sec-literal": "^1.1.0"
+ }
+}
diff --git a/workspaces/estree-ast-utils/src/arrayExpressionToString.js b/workspaces/estree-ast-utils/src/arrayExpressionToString.js
new file mode 100644
index 0000000..548154c
--- /dev/null
+++ b/workspaces/estree-ast-utils/src/arrayExpressionToString.js
@@ -0,0 +1,37 @@
+// Import Internal Dependencies
+import { VariableTracer } from "./utils/VariableTracer.js";
+
+/**
+ * @param {*} node
+ * @param {object} options
+ * @param {VariableTracer} [options.tracer=null]
+ * @returns {IterableIterator}
+ */
+
+export function* arrayExpressionToString(node, options = {}) {
+ const { tracer = null } = options;
+
+ if (!node || node.type !== "ArrayExpression") {
+ return;
+ }
+
+ for (const row of node.elements) {
+ switch (row.type) {
+ case "Literal": {
+ if (row.value === "") {
+ continue;
+ }
+
+ const value = Number(row.value);
+ yield Number.isNaN(value) ? row.value : String.fromCharCode(value);
+ break;
+ }
+ case "Identifier": {
+ if (tracer !== null && tracer.literalIdentifiers.has(row.name)) {
+ yield tracer.literalIdentifiers.get(row.name);
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/workspaces/estree-ast-utils/src/concatBinaryExpression.js b/workspaces/estree-ast-utils/src/concatBinaryExpression.js
new file mode 100644
index 0000000..590c96c
--- /dev/null
+++ b/workspaces/estree-ast-utils/src/concatBinaryExpression.js
@@ -0,0 +1,57 @@
+// Import Internal Dependencies
+import { arrayExpressionToString } from "./arrayExpressionToString.js";
+import { VariableTracer } from "./utils/VariableTracer.js";
+
+// CONSTANTS
+const kBinaryExprTypes = new Set([
+ "Literal",
+ "BinaryExpression",
+ "ArrayExpression",
+ "Identifier"
+]);
+
+/**
+ * @param {*} node
+ * @param {object} options
+ * @param {VariableTracer} [options.tracer=null]
+ * @param {boolean} [options.stopOnUnsupportedNode=false]
+ * @returns {IterableIterator}
+ */
+export function* concatBinaryExpression(node, options = {}) {
+ const {
+ tracer = null,
+ stopOnUnsupportedNode = false
+ } = options;
+ const { left, right } = node;
+
+ if (
+ stopOnUnsupportedNode &&
+ (!kBinaryExprTypes.has(left.type) || !kBinaryExprTypes.has(right.type))
+ ) {
+ throw new Error("concatBinaryExpression:: Unsupported node detected");
+ }
+
+ for (const childNode of [left, right]) {
+ switch (childNode.type) {
+ case "BinaryExpression": {
+ yield* concatBinaryExpression(childNode, {
+ tracer,
+ stopOnUnsupportedNode
+ });
+ break;
+ }
+ case "ArrayExpression": {
+ yield* arrayExpressionToString(childNode, { tracer });
+ break;
+ }
+ case "Literal":
+ yield childNode.value;
+ break;
+ case "Identifier":
+ if (tracer !== null && tracer.literalIdentifiers.has(childNode.name)) {
+ yield tracer.literalIdentifiers.get(childNode.name);
+ }
+ break;
+ }
+ }
+}
diff --git a/workspaces/estree-ast-utils/src/getCallExpressionArguments.js b/workspaces/estree-ast-utils/src/getCallExpressionArguments.js
new file mode 100644
index 0000000..f3f8b69
--- /dev/null
+++ b/workspaces/estree-ast-utils/src/getCallExpressionArguments.js
@@ -0,0 +1,45 @@
+// Import Third-party Dependencies
+import { Hex } from "@nodesecure/sec-literal";
+
+// Import Internal Dependencies
+import { concatBinaryExpression } from "./concatBinaryExpression.js";
+
+export function getCallExpressionArguments(node, options = {}) {
+ const { tracer = null } = options;
+
+ if (node.type !== "CallExpression" || node.arguments.length === 0) {
+ return null;
+ }
+
+ const literalsNode = [];
+ for (const arg of node.arguments) {
+ switch (arg.type) {
+ case "Identifier": {
+ if (tracer !== null && tracer.literalIdentifiers.has(arg.name)) {
+ literalsNode.push(tracer.literalIdentifiers.get(arg.name));
+ }
+
+ break;
+ }
+ case "Literal": {
+ literalsNode.push(hexToString(arg.value));
+
+ break;
+ }
+ case "BinaryExpression": {
+ const concatenatedBinaryExpr = [...concatBinaryExpression(arg, { tracer })].join("");
+ if (concatenatedBinaryExpr !== "") {
+ literalsNode.push(concatenatedBinaryExpr);
+ }
+
+ break;
+ }
+ }
+ }
+
+ return literalsNode.length === 0 ? null : literalsNode;
+}
+
+function hexToString(value) {
+ return Hex.isHex(value) ? Buffer.from(value, "hex").toString() : value;
+}
diff --git a/workspaces/estree-ast-utils/src/getCallExpressionIdentifier.js b/workspaces/estree-ast-utils/src/getCallExpressionIdentifier.js
new file mode 100644
index 0000000..1f9f80c
--- /dev/null
+++ b/workspaces/estree-ast-utils/src/getCallExpressionIdentifier.js
@@ -0,0 +1,21 @@
+// Import Internal Dependencies
+import { getMemberExpressionIdentifier } from "./getMemberExpressionIdentifier.js";
+
+/**
+ * @param {any} node
+ * @returns {string | null}
+ */
+export function getCallExpressionIdentifier(node) {
+ if (node.type !== "CallExpression") {
+ return null;
+ }
+
+ if (node.callee.type === "Identifier") {
+ return node.callee.name;
+ }
+ if (node.callee.type === "MemberExpression") {
+ return [...getMemberExpressionIdentifier(node.callee)].join(".");
+ }
+
+ return getCallExpressionIdentifier(node.callee);
+}
diff --git a/workspaces/estree-ast-utils/src/getMemberExpressionIdentifier.js b/workspaces/estree-ast-utils/src/getMemberExpressionIdentifier.js
new file mode 100644
index 0000000..799dd56
--- /dev/null
+++ b/workspaces/estree-ast-utils/src/getMemberExpressionIdentifier.js
@@ -0,0 +1,67 @@
+// Import Third-party Dependencies
+import { Hex } from "@nodesecure/sec-literal";
+
+// Import Internal Dependencies
+import { concatBinaryExpression } from "./concatBinaryExpression.js";
+import { VariableTracer } from "./utils/VariableTracer.js";
+
+/**
+ * Return the complete identifier of a MemberExpression
+ *
+ * @param {any} node
+ * @param {object} options
+ * @param {VariableTracer} [options.tracer=null]
+ * @returns {IterableIterator}
+ */
+export function* getMemberExpressionIdentifier(node, options = {}) {
+ const { tracer = null } = options;
+
+ switch (node.object.type) {
+ // Chain with another MemberExpression
+ case "MemberExpression":
+ yield* getMemberExpressionIdentifier(node.object, options);
+ break;
+ case "Identifier":
+ yield node.object.name;
+ break;
+ // Literal is used when the property is computed
+ case "Literal":
+ yield node.object.value;
+ break;
+ }
+
+ switch (node.property.type) {
+ case "Identifier": {
+ if (tracer !== null && tracer.literalIdentifiers.has(node.property.name)) {
+ yield tracer.literalIdentifiers.get(node.property.name);
+ }
+ else {
+ yield node.property.name;
+ }
+
+ break;
+ }
+ // Literal is used when the property is computed
+ case "Literal":
+ yield node.property.value;
+ break;
+
+ // foo.bar[callexpr()]
+ case "CallExpression": {
+ const args = node.property.arguments;
+ if (args.length > 0 && args[0].type === "Literal" && Hex.isHex(args[0].value)) {
+ yield Buffer.from(args[0].value, "hex").toString();
+ }
+ break;
+ }
+
+ // foo.bar["k" + "e" + "y"]
+ case "BinaryExpression": {
+ const literal = [...concatBinaryExpression(node.property, options)].join("");
+ if (literal.trim() !== "") {
+ yield literal;
+ }
+ break;
+ }
+ }
+}
diff --git a/workspaces/estree-ast-utils/src/getVariableDeclarationIdentifiers.js b/workspaces/estree-ast-utils/src/getVariableDeclarationIdentifiers.js
new file mode 100644
index 0000000..acf2319
--- /dev/null
+++ b/workspaces/estree-ast-utils/src/getVariableDeclarationIdentifiers.js
@@ -0,0 +1,110 @@
+// Import Internal Dependencies
+import { notNullOrUndefined } from "./utils/index.js";
+
+export function* getVariableDeclarationIdentifiers(node, options = {}) {
+ const { prefix = null } = options;
+
+ switch (node.type) {
+ case "VariableDeclaration": {
+ for (const variableDeclarator of node.declarations) {
+ yield* getVariableDeclarationIdentifiers(variableDeclarator.id);
+ }
+
+ break;
+ }
+
+ case "VariableDeclarator":
+ yield* getVariableDeclarationIdentifiers(node.id);
+
+ break;
+
+ case "Identifier":
+ yield { name: autoPrefix(node.name, prefix), assignmentId: node };
+
+ break;
+
+ case "Property": {
+ if (node.kind !== "init") {
+ break;
+ }
+
+ if (node.value.type === "ObjectPattern" || node.value.type === "ArrayPattern") {
+ yield* getVariableDeclarationIdentifiers(node.value, {
+ prefix: autoPrefix(node.key.name, prefix)
+ });
+ break;
+ }
+
+ let assignmentId = node.key;
+ if (node.value.type === "Identifier") {
+ assignmentId = node.value;
+ }
+ else if (node.value.type === "AssignmentPattern") {
+ assignmentId = node.value.left;
+ }
+
+ yield { name: autoPrefix(node.key.name, prefix), assignmentId };
+
+ break;
+ }
+
+ /**
+ * Rest syntax (in ArrayPattern or ObjectPattern for example)
+ * const [...foo] = []
+ * const {...foo} = {}
+ */
+ case "RestElement":
+ yield { name: autoPrefix(node.argument.name, prefix), assignmentId: node.argument };
+
+ break;
+
+ /**
+ * (foo = 5)
+ */
+ case "AssignmentExpression":
+ yield* getVariableDeclarationIdentifiers(node.left);
+
+ break;
+
+ /**
+ * const [{ foo }] = []
+ * const [foo = 10] = []
+ * ↪ Destructuration + Assignement of a default value
+ */
+ case "AssignmentPattern":
+ if (node.left.type === "Identifier") {
+ yield node.left.name;
+ }
+ else {
+ yield* getVariableDeclarationIdentifiers(node.left);
+ }
+
+ break;
+
+ /**
+ * const [foo] = [];
+ * ↪ Destructuration of foo is an ArrayPattern
+ */
+ case "ArrayPattern":
+ yield* node.elements
+ .filter(notNullOrUndefined)
+ .map((id) => [...getVariableDeclarationIdentifiers(id)]).flat();
+
+ break;
+
+ /**
+ * const {foo} = {};
+ * ↪ Destructuration of foo is an ObjectPattern
+ */
+ case "ObjectPattern":
+ yield* node.properties
+ .filter(notNullOrUndefined)
+ .map((property) => [...getVariableDeclarationIdentifiers(property)]).flat();
+
+ break;
+ }
+}
+
+function autoPrefix(name, prefix = null) {
+ return typeof prefix === "string" ? `${prefix}.${name}` : name;
+}
diff --git a/workspaces/estree-ast-utils/src/index.js b/workspaces/estree-ast-utils/src/index.js
new file mode 100644
index 0000000..f747004
--- /dev/null
+++ b/workspaces/estree-ast-utils/src/index.js
@@ -0,0 +1,9 @@
+export * from "./getMemberExpressionIdentifier.js";
+export * from "./getCallExpressionIdentifier.js";
+export * from "./getVariableDeclarationIdentifiers.js";
+export * from "./getCallExpressionArguments.js";
+export * from "./concatBinaryExpression.js";
+export * from "./arrayExpressionToString.js";
+export * from "./isLiteralRegex.js";
+
+export * from "./utils/VariableTracer.js";
diff --git a/workspaces/estree-ast-utils/src/isLiteralRegex.js b/workspaces/estree-ast-utils/src/isLiteralRegex.js
new file mode 100644
index 0000000..b73beff
--- /dev/null
+++ b/workspaces/estree-ast-utils/src/isLiteralRegex.js
@@ -0,0 +1,3 @@
+export function isLiteralRegex(node) {
+ return node.type === "Literal" && "regex" in node;
+}
diff --git a/workspaces/estree-ast-utils/src/utils/VariableTracer.js b/workspaces/estree-ast-utils/src/utils/VariableTracer.js
new file mode 100644
index 0000000..965a48f
--- /dev/null
+++ b/workspaces/estree-ast-utils/src/utils/VariableTracer.js
@@ -0,0 +1,411 @@
+// Import Node.js Dependencies
+import { EventEmitter } from "node:events";
+
+// Import Internal Dependencies
+import { notNullOrUndefined } from "./notNullOrUndefined.js";
+import { isEvilIdentifierPath, isNeutralCallable } from "./isEvilIdentifierPath.js";
+import { getSubMemberExpressionSegments } from "./getSubMemberExpressionSegments.js";
+import { getMemberExpressionIdentifier } from "../getMemberExpressionIdentifier.js";
+import { getCallExpressionIdentifier } from "../getCallExpressionIdentifier.js";
+import { getVariableDeclarationIdentifiers } from "../getVariableDeclarationIdentifiers.js";
+import { getCallExpressionArguments } from "../getCallExpressionArguments.js";
+
+// CONSTANTS
+const kGlobalIdentifiersToTrace = new Set([
+ "global", "globalThis", "root", "GLOBAL", "window"
+]);
+const kRequirePatterns = new Set([
+ "require", "require.resolve", "require.main", "process.mainModule.require"
+]);
+const kUnsafeGlobalCallExpression = new Set(["eval", "Function"]);
+
+export class VariableTracer extends EventEmitter {
+ static AssignmentEvent = Symbol("AssignmentEvent");
+
+ // PUBLIC PROPERTIES
+ /** @type {Map} */
+ literalIdentifiers = new Map();
+
+ /** @type {Set} */
+ importedModules = new Set();
+
+ // PRIVATE PROPERTIES
+ #traced = new Map();
+ #variablesRefToGlobal = new Set();
+
+ /** @type {Set} */
+ #neutralCallable = new Set();
+
+ debug() {
+ console.log(this.#traced);
+ }
+
+ enableDefaultTracing() {
+ [...kRequirePatterns]
+ .forEach((pattern) => this.trace(pattern, { followConsecutiveAssignment: true, name: "require" }));
+
+ return this
+ .trace("eval")
+ .trace("Function")
+ .trace("atob", { followConsecutiveAssignment: true });
+ }
+
+ /**
+ *
+ * @param {!string} identifierOrMemberExpr
+ * @param {object} [options]
+ * @param {string} [options.name]
+ * @param {string} [options.moduleName=null]
+ * @param {boolean} [options.followConsecutiveAssignment=false]
+ *
+ * @example
+ * new VariableTracer()
+ * .trace("require", { followConsecutiveAssignment: true })
+ * .trace("process.mainModule")
+ */
+ trace(identifierOrMemberExpr, options = {}) {
+ const {
+ followConsecutiveAssignment = false,
+ moduleName = null,
+ name = identifierOrMemberExpr
+ } = options;
+
+ this.#traced.set(identifierOrMemberExpr, {
+ name,
+ identifierOrMemberExpr,
+ followConsecutiveAssignment,
+ assignmentMemory: [],
+ moduleName
+ });
+
+ if (identifierOrMemberExpr.includes(".")) {
+ const exprs = [...getSubMemberExpressionSegments(identifierOrMemberExpr)]
+ .filter((expr) => !this.#traced.has(expr));
+
+ for (const expr of exprs) {
+ this.trace(expr, {
+ followConsecutiveAssignment: true, name, moduleName
+ });
+ }
+ }
+
+ return this;
+ }
+
+ /**
+ * @param {!string} identifierOrMemberExpr An identifier like "foo" or "foo.bar"
+ */
+ getDataFromIdentifier(identifierOrMemberExpr) {
+ const isMemberExpr = identifierOrMemberExpr.includes(".");
+ const isTracingIdentifier = this.#traced.has(identifierOrMemberExpr);
+
+ let finalIdentifier = identifierOrMemberExpr;
+ if (isMemberExpr && !isTracingIdentifier) {
+ const [segment] = identifierOrMemberExpr.split(".");
+ if (this.#traced.has(segment)) {
+ const tracedIdentifier = this.#traced.get(segment);
+ finalIdentifier = `${tracedIdentifier.identifierOrMemberExpr}${identifierOrMemberExpr.slice(segment.length)}`;
+ }
+
+ if (!this.#traced.has(finalIdentifier)) {
+ return null;
+ }
+ }
+ else if (!isTracingIdentifier) {
+ return null;
+ }
+
+ const tracedIdentifier = this.#traced.get(finalIdentifier);
+ if (!this.#isTracedIdentifierImportedAsModule(tracedIdentifier)) {
+ return null;
+ }
+
+ const assignmentMemory = this.#traced.get(tracedIdentifier.name)?.assignmentMemory ?? [];
+
+ return {
+ name: tracedIdentifier.name,
+ identifierOrMemberExpr: tracedIdentifier.identifierOrMemberExpr,
+ assignmentMemory
+ };
+ }
+
+ #getTracedName(identifierOrMemberExpr) {
+ return this.#traced.has(identifierOrMemberExpr) ?
+ this.#traced.get(identifierOrMemberExpr).name : null;
+ }
+
+ #isTracedIdentifierImportedAsModule(id) {
+ return id.moduleName === null || this.importedModules.has(id.moduleName);
+ }
+
+ #declareNewAssignment(identifierOrMemberExpr, id) {
+ const tracedVariant = this.#traced.get(identifierOrMemberExpr);
+
+ // We return if required module has not been imported
+ // It mean the assigment has no relation with the required tracing
+ if (!this.#isTracedIdentifierImportedAsModule(tracedVariant)) {
+ return;
+ }
+
+ const newIdentiferName = id.name;
+
+ const assignmentEventPayload = {
+ name: tracedVariant.name,
+ identifierOrMemberExpr: tracedVariant.identifierOrMemberExpr,
+ id: newIdentiferName,
+ location: id.loc
+ };
+ this.emit(VariableTracer.AssignmentEvent, assignmentEventPayload);
+ this.emit(tracedVariant.identifierOrMemberExpr, assignmentEventPayload);
+
+ if (tracedVariant.followConsecutiveAssignment && !this.#traced.has(newIdentiferName)) {
+ this.#traced.get(tracedVariant.name).assignmentMemory.push(newIdentiferName);
+ this.#traced.set(newIdentiferName, tracedVariant);
+ }
+ }
+
+ #isGlobalVariableIdentifier(identifierName) {
+ return kGlobalIdentifiersToTrace.has(identifierName) ||
+ this.#variablesRefToGlobal.has(identifierName);
+ }
+
+ /**
+ * Search alternative for the given MemberExpression parts
+ *
+ * @example
+ * const { process: aName } = globalThis;
+ * const boo = aName.mainModule.require; // alternative: process.mainModule.require
+ */
+ #searchForMemberExprAlternative(parts = []) {
+ return parts.flatMap((identifierName) => {
+ if (this.#traced.has(identifierName)) {
+ return this.#traced.get(identifierName).identifierOrMemberExpr;
+ }
+
+ /**
+ * If identifier is global then we can eliminate the value from MemberExpr
+ *
+ * globalThis.process === process;
+ */
+ if (this.#isGlobalVariableIdentifier(identifierName)) {
+ return [];
+ }
+
+ return identifierName;
+ });
+ }
+
+ #autoTraceId(id, prefix = null) {
+ for (const { name, assignmentId } of getVariableDeclarationIdentifiers(id)) {
+ const identifierOrMemberExpr = typeof prefix === "string" ? `${prefix}.${name}` : name;
+
+ if (this.#traced.has(identifierOrMemberExpr)) {
+ this.#declareNewAssignment(identifierOrMemberExpr, assignmentId);
+ }
+ }
+ }
+
+ #walkImportDeclaration(node) {
+ const moduleName = node.source.value;
+ if (!this.#traced.has(moduleName)) {
+ return;
+ }
+
+ this.importedModules.add(moduleName);
+
+ // import * as boo from "crypto";
+ if (node.specifiers[0].type === "ImportNamespaceSpecifier") {
+ const importNamespaceNode = node.specifiers[0];
+ this.#declareNewAssignment(moduleName, importNamespaceNode.local);
+
+ return;
+ }
+
+ // import { createHash } from "crypto";
+ const importSpecifiers = node.specifiers
+ .filter((specifierNode) => specifierNode.type === "ImportSpecifier");
+ for (const specifier of importSpecifiers) {
+ const fullImportedName = `${moduleName}.${specifier.imported.name}`;
+
+ if (this.#traced.has(fullImportedName)) {
+ this.#declareNewAssignment(fullImportedName, specifier.imported);
+ }
+ }
+ }
+
+ #walkRequireCallExpression(variableDeclaratorNode) {
+ const { init, id } = variableDeclaratorNode;
+
+ const moduleNameLiteral = init.arguments
+ .find((argumentNode) => argumentNode.type === "Literal" && this.#traced.has(argumentNode.value));
+ if (!moduleNameLiteral) {
+ return;
+ }
+ this.importedModules.add(moduleNameLiteral.value);
+
+ switch (id.type) {
+ case "Identifier":
+ this.#declareNewAssignment(moduleNameLiteral.value, id);
+ break;
+ case "ObjectPattern": {
+ this.#autoTraceId(id, moduleNameLiteral.value);
+
+ break;
+ }
+ }
+ }
+
+ #walkVariableDeclarationWithIdentifier(variableDeclaratorNode) {
+ const { init, id } = variableDeclaratorNode;
+
+ switch (init.type) {
+ // let foo = "10"; <-- "foo" is the key and "10" the value
+ case "Literal":
+ this.literalIdentifiers.set(id.name, init.value);
+ break;
+
+ // const g = eval("this");
+ case "CallExpression": {
+ const fullIdentifierPath = getCallExpressionIdentifier(init);
+ if (fullIdentifierPath === null) {
+ break;
+ }
+
+ const tracedFullIdentifierName = this.#getTracedName(fullIdentifierPath) ?? fullIdentifierPath;
+ const [identifierName] = fullIdentifierPath.split(".");
+
+ // const id = Function.prototype.call.call(require, require, "http");
+ if (this.#neutralCallable.has(identifierName) || isEvilIdentifierPath(fullIdentifierPath)) {
+ // TODO: make sure we are walking on a require CallExpr here ?
+ this.#walkRequireCallExpression(variableDeclaratorNode);
+ }
+ else if (kUnsafeGlobalCallExpression.has(identifierName)) {
+ this.#variablesRefToGlobal.add(id.name);
+ }
+ // const foo = require("crypto");
+ // const bar = require.call(null, "crypto");
+ else if (kRequirePatterns.has(identifierName)) {
+ this.#walkRequireCallExpression(variableDeclaratorNode);
+ }
+ else if (tracedFullIdentifierName === "atob") {
+ const callExprArguments = getCallExpressionArguments(init, { tracer: this });
+ if (callExprArguments === null) {
+ break;
+ }
+
+ const callExprArgumentNode = callExprArguments.at(0);
+ if (typeof callExprArgumentNode === "string") {
+ this.literalIdentifiers.set(
+ id.name,
+ Buffer.from(callExprArgumentNode, "base64").toString()
+ );
+ }
+ }
+
+ break;
+ }
+
+ // const r = require
+ case "Identifier": {
+ const identifierName = init.name;
+ if (this.#traced.has(identifierName)) {
+ this.#declareNewAssignment(identifierName, variableDeclaratorNode.id);
+ }
+ else if (this.#isGlobalVariableIdentifier(identifierName)) {
+ this.#variablesRefToGlobal.add(id.name);
+ }
+
+ break;
+ }
+
+ // process.mainModule and require.resolve
+ case "MemberExpression": {
+ // Example: ["process", "mainModule"]
+ const memberExprParts = [...getMemberExpressionIdentifier(init, { tracer: this })];
+ const memberExprFullname = memberExprParts.join(".");
+
+ // Function.prototype.call
+ if (isNeutralCallable(memberExprFullname)) {
+ this.#neutralCallable.add(variableDeclaratorNode.id.name);
+ }
+ else if (this.#traced.has(memberExprFullname)) {
+ this.#declareNewAssignment(memberExprFullname, variableDeclaratorNode.id);
+ }
+ else {
+ const alternativeMemberExprParts = this.#searchForMemberExprAlternative(memberExprParts);
+ const alternativeMemberExprFullname = alternativeMemberExprParts.join(".");
+
+ if (this.#traced.has(alternativeMemberExprFullname)) {
+ this.#declareNewAssignment(alternativeMemberExprFullname, variableDeclaratorNode.id);
+ }
+ }
+
+ break;
+ }
+ }
+ }
+
+ #walkVariableDeclarationWithAnythingElse(variableDeclaratorNode) {
+ const { init, id } = variableDeclaratorNode;
+
+ switch (init.type) {
+ // const { process } = eval("this");
+ case "CallExpression": {
+ const fullIdentifierPath = getCallExpressionIdentifier(init);
+ if (fullIdentifierPath === null) {
+ break;
+ }
+ const [identifierName] = fullIdentifierPath.split(".");
+
+ // const {} = Function.prototype.call.call(require, require, "http");
+ if (isEvilIdentifierPath(fullIdentifierPath)) {
+ this.#walkRequireCallExpression(variableDeclaratorNode);
+ }
+ else if (kUnsafeGlobalCallExpression.has(identifierName)) {
+ this.#autoTraceId(id);
+ }
+ // const { createHash } = require("crypto");
+ else if (kRequirePatterns.has(identifierName)) {
+ this.#walkRequireCallExpression(variableDeclaratorNode);
+ }
+
+ break;
+ }
+
+ // const { process } = globalThis;
+ case "Identifier": {
+ const identifierName = init.name;
+ if (this.#isGlobalVariableIdentifier(identifierName)) {
+ this.#autoTraceId(id);
+ }
+
+ break;
+ }
+ }
+ }
+
+ walk(node) {
+ switch (node.type) {
+ case "ImportDeclaration": {
+ this.#walkImportDeclaration(node);
+ break;
+ }
+ case "VariableDeclaration": {
+ for (const variableDeclaratorNode of node.declarations) {
+ // var foo; <-- no initialization here.
+ if (!notNullOrUndefined(variableDeclaratorNode.init)) {
+ continue;
+ }
+
+ if (variableDeclaratorNode.id.type === "Identifier") {
+ this.#walkVariableDeclarationWithIdentifier(variableDeclaratorNode);
+ }
+ else {
+ this.#walkVariableDeclarationWithAnythingElse(variableDeclaratorNode);
+ }
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/workspaces/estree-ast-utils/src/utils/getSubMemberExpressionSegments.js b/workspaces/estree-ast-utils/src/utils/getSubMemberExpressionSegments.js
new file mode 100644
index 0000000..e4ccc3f
--- /dev/null
+++ b/workspaces/estree-ast-utils/src/utils/getSubMemberExpressionSegments.js
@@ -0,0 +1,13 @@
+/**
+ * @param {!string} str
+ * @returns {IterableIterator}
+ */
+export function* getSubMemberExpressionSegments(memberExpressionFullpath) {
+ const identifiers = memberExpressionFullpath.split(".");
+ const segments = [];
+
+ for (let i = 0; i < identifiers.length - 1; i++) {
+ segments.push(identifiers[i]);
+ yield segments.join(".");
+ }
+}
diff --git a/workspaces/estree-ast-utils/src/utils/index.js b/workspaces/estree-ast-utils/src/utils/index.js
new file mode 100644
index 0000000..d4b343b
--- /dev/null
+++ b/workspaces/estree-ast-utils/src/utils/index.js
@@ -0,0 +1,4 @@
+export * from "./getSubMemberExpressionSegments.js";
+export * from "./notNullOrUndefined.js";
+export * from "./VariableTracer.js";
+export * from "./isEvilIdentifierPath.js";
diff --git a/workspaces/estree-ast-utils/src/utils/isEvilIdentifierPath.js b/workspaces/estree-ast-utils/src/utils/isEvilIdentifierPath.js
new file mode 100644
index 0000000..e9671c2
--- /dev/null
+++ b/workspaces/estree-ast-utils/src/utils/isEvilIdentifierPath.js
@@ -0,0 +1,18 @@
+/**
+ * @param {!string} identifier
+ */
+export function isEvilIdentifierPath(identifier) {
+ return isFunctionPrototype(identifier);
+}
+
+export function isNeutralCallable(identifier) {
+ return identifier === "Function.prototype.call";
+}
+
+/**
+ * @param {!string} identifier
+ */
+function isFunctionPrototype(identifier) {
+ return identifier.startsWith("Function.prototype")
+ && /call|apply|bind/i.test(identifier);
+}
diff --git a/workspaces/estree-ast-utils/src/utils/notNullOrUndefined.js b/workspaces/estree-ast-utils/src/utils/notNullOrUndefined.js
new file mode 100644
index 0000000..9eee585
--- /dev/null
+++ b/workspaces/estree-ast-utils/src/utils/notNullOrUndefined.js
@@ -0,0 +1,3 @@
+export function notNullOrUndefined(value) {
+ return value !== null && value !== void 0;
+}
diff --git a/workspaces/estree-ast-utils/test/VariableTracer/VariableTracer.spec.js b/workspaces/estree-ast-utils/test/VariableTracer/VariableTracer.spec.js
new file mode 100644
index 0000000..41f9ce5
--- /dev/null
+++ b/workspaces/estree-ast-utils/test/VariableTracer/VariableTracer.spec.js
@@ -0,0 +1,145 @@
+// Import Third-party Dependencies
+import test from "tape";
+
+// Import Internal Dependencies
+import { createTracer } from "../utils.js";
+
+test("getDataFromIdentifier must return primitive null is there is no kwown traced identifier", (tape) => {
+ const helpers = createTracer(true);
+
+ const result = helpers.tracer.getDataFromIdentifier("foobar");
+
+ tape.strictEqual(result, null);
+ tape.end();
+});
+
+test("it should be able to Trace a malicious code with Global, BinaryExpr, Assignments and Hexadecimal", (tape) => {
+ const helpers = createTracer(true);
+ const assignments = helpers.getAssignmentArray();
+
+ helpers.walkOnCode(`
+ var foo;
+ const g = eval("this");
+ const p = g["pro" + "cess"];
+
+ const evil = p["mainMod" + "ule"][unhex("72657175697265")];
+ const work = evil(unhex("2e2f746573742f64617461"))
+ `);
+
+ const evil = helpers.tracer.getDataFromIdentifier("evil");
+ tape.deepEqual(evil, {
+ name: "require",
+ identifierOrMemberExpr: "process.mainModule.require",
+ assignmentMemory: ["p", "evil"]
+ });
+ tape.strictEqual(assignments.length, 2);
+
+ const [eventOne, eventTwo] = assignments;
+ tape.strictEqual(eventOne.identifierOrMemberExpr, "process");
+ tape.strictEqual(eventOne.id, "p");
+
+ tape.strictEqual(eventTwo.identifierOrMemberExpr, "process.mainModule.require");
+ tape.strictEqual(eventTwo.id, "evil");
+
+ tape.end();
+});
+
+test("it should be able to Trace a malicious CallExpression by recombining segments of the MemberExpression", (tape) => {
+ const helpers = createTracer(true);
+ const assignments = helpers.getAssignmentArray();
+
+ helpers.walkOnCode(`
+ const g = global.process;
+ const r = g.mainModule;
+ const c = r.require;
+ c("http");
+ r.require("fs");
+ `);
+
+ const evil = helpers.tracer.getDataFromIdentifier("r.require");
+ tape.deepEqual(evil, {
+ name: "require",
+ identifierOrMemberExpr: "process.mainModule.require",
+ assignmentMemory: ["g", "r", "c"]
+ });
+ tape.strictEqual(assignments.length, 3);
+
+ const [eventOne, eventTwo, eventThree] = assignments;
+ tape.strictEqual(eventOne.identifierOrMemberExpr, "process");
+ tape.strictEqual(eventOne.id, "g");
+
+ tape.strictEqual(eventTwo.identifierOrMemberExpr, "process.mainModule");
+ tape.strictEqual(eventTwo.id, "r");
+
+ tape.strictEqual(eventThree.identifierOrMemberExpr, "process.mainModule.require");
+ tape.strictEqual(eventThree.id, "c");
+
+ tape.end();
+});
+
+test("given a MemberExpression segment that doesn't match anything then it should return null", (tape) => {
+ const helpers = createTracer(true);
+
+ const result = helpers.tracer.getDataFromIdentifier("foo.bar");
+ tape.strictEqual(result, null);
+
+ tape.end();
+});
+
+test("it should be able to Trace a require using Function.prototype.call", (tape) => {
+ const helpers = createTracer();
+ helpers.tracer.trace("http");
+ const assignments = helpers.getAssignmentArray();
+
+ helpers.walkOnCode(`
+ const proto = Function.prototype.call.call(require, require, "http");
+ `);
+
+ const proto = helpers.tracer.getDataFromIdentifier("proto");
+
+ tape.strictEqual(proto, null);
+ tape.strictEqual(assignments.length, 1);
+
+ const [eventOne] = assignments;
+ tape.strictEqual(eventOne.identifierOrMemberExpr, "http");
+ tape.strictEqual(eventOne.id, "proto");
+
+ tape.end();
+});
+
+test("it should be able to Trace an unsafe crypto.createHash using Function.prototype.call reassignment", (tape) => {
+ const helpers = createTracer(true);
+ helpers.tracer.trace("crypto.createHash", { followConsecutiveAssignment: true });
+ const assignments = helpers.getAssignmentArray();
+
+ helpers.walkOnCode(`
+ const aA = Function.prototype.call;
+ const bB = require;
+
+ const crr = aA.call(bB, bB, "crypto");
+ const createHashBis = crr.createHash;
+ createHashBis("md5");
+ `);
+
+ const createHashBis = helpers.tracer.getDataFromIdentifier("createHashBis");
+ tape.deepEqual(createHashBis, {
+ name: "crypto.createHash",
+ identifierOrMemberExpr: "crypto.createHash",
+ assignmentMemory: ["crr", "createHashBis"]
+ });
+
+ tape.strictEqual(helpers.tracer.importedModules.has("crypto"), true);
+ tape.strictEqual(assignments.length, 3);
+
+ const [eventOne, eventTwo, eventThree] = assignments;
+ tape.strictEqual(eventOne.identifierOrMemberExpr, "require");
+ tape.strictEqual(eventOne.id, "bB");
+
+ tape.strictEqual(eventTwo.identifierOrMemberExpr, "crypto");
+ tape.strictEqual(eventTwo.id, "crr");
+
+ tape.strictEqual(eventThree.identifierOrMemberExpr, "crypto.createHash");
+ tape.strictEqual(eventThree.id, "createHashBis");
+
+ tape.end();
+});
diff --git a/workspaces/estree-ast-utils/test/VariableTracer/assignments.spec.js b/workspaces/estree-ast-utils/test/VariableTracer/assignments.spec.js
new file mode 100644
index 0000000..e5f6833
--- /dev/null
+++ b/workspaces/estree-ast-utils/test/VariableTracer/assignments.spec.js
@@ -0,0 +1,134 @@
+
+// Import Third-party Dependencies
+import test from "tape";
+
+// Import Internal Dependencies
+import { createTracer } from "../utils.js";
+
+test("it should be able to Trace a require Assignment (using a global variable)", (tape) => {
+ const helpers = createTracer(true);
+ const assignments = helpers.getAssignmentArray();
+
+ helpers.walkOnCode(`
+ const test = globalThis;
+ const foo = test.require;
+ foo("http");
+ `);
+
+ const foo = helpers.tracer.getDataFromIdentifier("foo");
+ tape.deepEqual(foo, {
+ name: "require",
+ identifierOrMemberExpr: "require",
+ assignmentMemory: ["foo"]
+ });
+ tape.strictEqual(assignments.length, 1);
+
+ const [eventOne] = assignments;
+ tape.strictEqual(eventOne.identifierOrMemberExpr, "require");
+ tape.strictEqual(eventOne.id, "foo");
+
+ tape.end();
+});
+
+test("it should be able to Trace a require Assignment (using a MemberExpression)", (tape) => {
+ const helpers = createTracer(true);
+ const assignments = helpers.getAssignmentArray();
+
+ helpers.walkOnCode(`
+ const foo = require.resolve;
+ foo("http");
+ `);
+
+ const foo = helpers.tracer.getDataFromIdentifier("foo");
+ tape.deepEqual(foo, {
+ name: "require",
+ identifierOrMemberExpr: "require.resolve",
+ assignmentMemory: ["foo"]
+ });
+ tape.strictEqual(assignments.length, 1);
+
+ const [eventOne] = assignments;
+ tape.strictEqual(eventOne.identifierOrMemberExpr, "require.resolve");
+ tape.strictEqual(eventOne.id, "foo");
+
+ tape.end();
+});
+
+test("it should be able to Trace a global Assignment using an ESTree ObjectPattern", (tape) => {
+ const helpers = createTracer(true);
+ const assignments = helpers.getAssignmentArray();
+
+ helpers.walkOnCode(`
+ const { process: yoo } = globalThis;
+
+ const boo = yoo.mainModule.require;
+ `);
+
+ const boo = helpers.tracer.getDataFromIdentifier("boo");
+
+ tape.deepEqual(boo, {
+ name: "require",
+ identifierOrMemberExpr: "process.mainModule.require",
+ assignmentMemory: ["yoo", "boo"]
+ });
+ tape.strictEqual(assignments.length, 2);
+
+ const [eventOne, eventTwo] = assignments;
+ tape.strictEqual(eventOne.identifierOrMemberExpr, "process");
+ tape.strictEqual(eventOne.id, "yoo");
+
+ tape.strictEqual(eventTwo.identifierOrMemberExpr, "process.mainModule.require");
+ tape.strictEqual(eventTwo.id, "boo");
+
+ tape.end();
+});
+
+test("it should be able to Trace an Unsafe Function() Assignment using an ESTree ObjectPattern", (tape) => {
+ const helpers = createTracer(true);
+ const assignments = helpers.getAssignmentArray();
+
+ helpers.walkOnCode(`
+ const { process: yoo } = Function("return this")();
+
+ const boo = yoo.mainModule.require;
+ `);
+
+ const boo = helpers.tracer.getDataFromIdentifier("boo");
+
+ tape.deepEqual(boo, {
+ name: "require",
+ identifierOrMemberExpr: "process.mainModule.require",
+ assignmentMemory: ["yoo", "boo"]
+ });
+ tape.strictEqual(assignments.length, 2);
+
+ const [eventOne, eventTwo] = assignments;
+ tape.strictEqual(eventOne.identifierOrMemberExpr, "process");
+ tape.strictEqual(eventOne.id, "yoo");
+
+ tape.strictEqual(eventTwo.identifierOrMemberExpr, "process.mainModule.require");
+ tape.strictEqual(eventTwo.id, "boo");
+
+ tape.end();
+});
+
+test("it should be able to Trace a require Assignment with atob", (tape) => {
+ const helpers = createTracer(true);
+ const assignments = helpers.getAssignmentArray();
+
+ helpers.walkOnCode(`
+ const xo = atob;
+ const yo = 'b3M=';
+ const ff = xo(yo);
+ `);
+ tape.strictEqual(assignments.length, 1);
+
+ const [eventOne] = assignments;
+ tape.strictEqual(eventOne.identifierOrMemberExpr, "atob");
+ tape.strictEqual(eventOne.id, "xo");
+
+ tape.true(helpers.tracer.literalIdentifiers.has("ff"));
+ tape.strictEqual(helpers.tracer.literalIdentifiers.get("ff"), "os");
+
+ tape.end();
+});
diff --git a/workspaces/estree-ast-utils/test/VariableTracer/cryptoCreateHash.spec.js b/workspaces/estree-ast-utils/test/VariableTracer/cryptoCreateHash.spec.js
new file mode 100644
index 0000000..55f01bf
--- /dev/null
+++ b/workspaces/estree-ast-utils/test/VariableTracer/cryptoCreateHash.spec.js
@@ -0,0 +1,194 @@
+// Import Third-party Dependencies
+import test from "tape";
+
+// Import Internal Dependencies
+import { createTracer } from "../utils.js";
+
+test("it should be able to Trace crypto.createHash when imported with an ESTree ImportNamespaceSpecifier (ESM)", (tape) => {
+ const helpers = createTracer();
+ helpers.tracer.trace("crypto.createHash", {
+ followConsecutiveAssignment: true,
+ moduleName: "crypto"
+ });
+ const assignments = helpers.getAssignmentArray();
+
+ helpers.walkOnCode(`
+ import fs from "fs";
+ import * as cryptoBis from "crypto";
+
+ const createHashBis = cryptoBis.createHash;
+ createHashBis("md5");
+ `);
+
+ const createHashBis = helpers.tracer.getDataFromIdentifier("createHashBis");
+
+ tape.deepEqual(createHashBis, {
+ name: "crypto.createHash",
+ identifierOrMemberExpr: "crypto.createHash",
+ assignmentMemory: ["cryptoBis", "createHashBis"]
+ });
+ tape.strictEqual(assignments.length, 2);
+
+ const [eventOne, eventTwo] = assignments;
+ tape.strictEqual(eventOne.identifierOrMemberExpr, "crypto");
+ tape.strictEqual(eventOne.id, "cryptoBis");
+
+ tape.strictEqual(eventTwo.identifierOrMemberExpr, "crypto.createHash");
+ tape.strictEqual(eventTwo.id, "createHashBis");
+
+ tape.end();
+});
+
+test("it should be able to Trace createHash when required (CommonJS) and destructured with an ESTree ObjectPattern", (tape) => {
+ const helpers = createTracer();
+ helpers.tracer.trace("crypto.createHash", {
+ followConsecutiveAssignment: true,
+ moduleName: "crypto"
+ });
+ const assignments = helpers.getAssignmentArray();
+
+ /**
+ * This is an ObjectPattern:
+ * const { createHash } = ...
+ */
+ helpers.walkOnCode(`
+ const { createHash } = require("crypto");
+
+ const createHashBis = createHash;
+ createHashBis("md5");
+ `);
+
+ const createHashBis = helpers.tracer.getDataFromIdentifier("createHashBis");
+
+ tape.deepEqual(createHashBis, {
+ name: "crypto.createHash",
+ identifierOrMemberExpr: "crypto.createHash",
+ assignmentMemory: ["createHash", "createHashBis"]
+ });
+ tape.strictEqual(assignments.length, 2);
+
+ const [eventOne, eventTwo] = assignments;
+ tape.strictEqual(eventOne.identifierOrMemberExpr, "crypto.createHash");
+ tape.strictEqual(eventOne.id, "createHash");
+
+ tape.strictEqual(eventTwo.identifierOrMemberExpr, "crypto.createHash");
+ tape.strictEqual(eventTwo.id, "createHashBis");
+
+ tape.end();
+});
+
+test("it should be able to Trace crypto.createHash when imported with an ESTree ImportSpecifier (ESM)", (tape) => {
+ const helpers = createTracer();
+ helpers.tracer.trace("crypto.createHash", {
+ followConsecutiveAssignment: true,
+ moduleName: "crypto"
+ });
+ const assignments = helpers.getAssignmentArray();
+
+ helpers.walkOnCode(`
+ import { createHash } from "crypto";
+
+ const createHashBis = createHash;
+ createHashBis("md5");
+ `);
+
+ const createHashBis = helpers.tracer.getDataFromIdentifier("createHashBis");
+
+ tape.deepEqual(createHashBis, {
+ name: "crypto.createHash",
+ identifierOrMemberExpr: "crypto.createHash",
+ assignmentMemory: ["createHash", "createHashBis"]
+ });
+ tape.strictEqual(assignments.length, 2);
+
+ const [eventOne, eventTwo] = assignments;
+ tape.strictEqual(eventOne.identifierOrMemberExpr, "crypto.createHash");
+ tape.strictEqual(eventOne.id, "createHash");
+
+ tape.strictEqual(eventTwo.identifierOrMemberExpr, "crypto.createHash");
+ tape.strictEqual(eventTwo.id, "createHashBis");
+
+ tape.end();
+});
+
+test("it should be able to Trace crypto.createHash with CommonJS require and with a computed method with a Literal", (tape) => {
+ const helpers = createTracer();
+ helpers.tracer.trace("crypto.createHash", {
+ followConsecutiveAssignment: true,
+ moduleName: "crypto"
+ });
+ const assignments = helpers.getAssignmentArray();
+
+ helpers.walkOnCode(`
+ const fs = require("fs");
+ const crypto = require("crypto");
+
+ const id = "createHash";
+ const createHashBis = crypto[id];
+ createHashBis("md5");
+ `);
+
+ tape.strictEqual(helpers.tracer.importedModules.has("crypto"), true);
+
+ const createHashBis = helpers.tracer.getDataFromIdentifier("createHashBis");
+
+ tape.deepEqual(createHashBis, {
+ name: "crypto.createHash",
+ identifierOrMemberExpr: "crypto.createHash",
+ assignmentMemory: ["createHashBis"]
+ });
+ tape.strictEqual(assignments.length, 2);
+
+ const [eventOne, eventTwo] = assignments;
+ tape.strictEqual(eventOne.identifierOrMemberExpr, "crypto");
+ tape.strictEqual(eventOne.id, "crypto");
+
+ tape.strictEqual(eventTwo.identifierOrMemberExpr, "crypto.createHash");
+ tape.strictEqual(eventTwo.id, "createHashBis");
+
+ tape.end();
+});
+
+test("it should not detect variable assignment since the crypto module is not imported", (tape) => {
+ const helpers = createTracer();
+ helpers.tracer.trace("crypto.createHash", {
+ followConsecutiveAssignment: true,
+ moduleName: "crypto"
+ });
+
+ const assignments = helpers.getAssignmentArray();
+
+ helpers.walkOnCode(`
+ const crypto = {
+ createHash() {}
+ }
+ const _t = crypto.createHash;
+ _t("md5");
+ `);
+
+ tape.strictEqual(helpers.tracer.importedModules.has("crypto"), false);
+ tape.strictEqual(assignments.length, 0);
+
+ tape.end();
+});
+
+test("it should return null because crypto.createHash is not imported from a module", (tape) => {
+ const helpers = createTracer(true);
+ helpers.tracer.trace("crypto.createHash", {
+ followConsecutiveAssignment: true,
+ moduleName: "crypto"
+ });
+
+ helpers.walkOnCode(`
+ const crypto = {
+ createHash() {}
+ }
+ const evil = crypto.createHash;
+ evil('md5');
+ `);
+
+ const result = helpers.tracer.getDataFromIdentifier("crypto.createHash");
+ tape.strictEqual(result, null);
+
+ tape.end();
+});
diff --git a/workspaces/estree-ast-utils/test/arrayExpressionToString.spec.js b/workspaces/estree-ast-utils/test/arrayExpressionToString.spec.js
new file mode 100644
index 0000000..7de5156
--- /dev/null
+++ b/workspaces/estree-ast-utils/test/arrayExpressionToString.spec.js
@@ -0,0 +1,75 @@
+// Import Third-party Dependencies
+import test from "tape";
+import { IteratorMatcher } from "iterator-matcher";
+
+// Import Internal Dependencies
+import { arrayExpressionToString } from "../src/index.js";
+import { codeToAst, getExpressionFromStatement, createTracer } from "./utils.js";
+
+test("given an ArrayExpression with two Literals then the iterable must return them one by one", (tape) => {
+ const [astNode] = codeToAst("['foo', 'bar']");
+ const iter = arrayExpressionToString(getExpressionFromStatement(astNode));
+
+ const iterResult = new IteratorMatcher()
+ .expect("foo")
+ .expect("bar")
+ .execute(iter, { allowNoMatchingValues: false });
+
+ tape.strictEqual(iterResult.isMatching, true);
+ tape.strictEqual(iterResult.elapsedSteps, 2);
+ tape.end();
+});
+
+test("given an ArrayExpression with two Identifiers then the iterable must return value from the Tracer", (tape) => {
+ const { tracer } = createTracer();
+ tracer.literalIdentifiers.set("foo", "1");
+ tracer.literalIdentifiers.set("bar", "2");
+
+ const [astNode] = codeToAst("[foo, bar]");
+ const iter = arrayExpressionToString(getExpressionFromStatement(astNode), { tracer });
+
+ const iterResult = new IteratorMatcher()
+ .expect("1")
+ .expect("2")
+ .execute(iter, { allowNoMatchingValues: false });
+
+ tape.strictEqual(iterResult.isMatching, true);
+ tape.strictEqual(iterResult.elapsedSteps, 2);
+ tape.end();
+});
+
+test(`given an ArrayExpression with two numbers
+ then the function must convert them as char code
+ and return them in the iterable`, (tape) => {
+ const [astNode] = codeToAst("[65, 66]");
+ const iter = arrayExpressionToString(getExpressionFromStatement(astNode));
+
+ const iterResult = new IteratorMatcher()
+ .expect("A")
+ .expect("B")
+ .execute(iter, { allowNoMatchingValues: false });
+
+ tape.strictEqual(iterResult.isMatching, true);
+ tape.strictEqual(iterResult.elapsedSteps, 2);
+ tape.end();
+});
+
+test("given an ArrayExpression with empty Literals then the iterable must return no values", (tape) => {
+ const [astNode] = codeToAst("['', '']");
+ const iter = arrayExpressionToString(getExpressionFromStatement(astNode));
+
+ const iterResult = [...iter];
+
+ tape.strictEqual(iterResult.length, 0);
+ tape.end();
+});
+
+test("given an AST that is not an ArrayExpression then it must return immediately", (tape) => {
+ const [astNode] = codeToAst("const foo = 5;");
+ const iter = arrayExpressionToString(astNode);
+
+ const iterResult = [...iter];
+
+ tape.strictEqual(iterResult.length, 0);
+ tape.end();
+});
diff --git a/workspaces/estree-ast-utils/test/concatBinaryExpression.spec.js b/workspaces/estree-ast-utils/test/concatBinaryExpression.spec.js
new file mode 100644
index 0000000..630d5a9
--- /dev/null
+++ b/workspaces/estree-ast-utils/test/concatBinaryExpression.spec.js
@@ -0,0 +1,92 @@
+// Import Third-party Dependencies
+import test from "tape";
+import { IteratorMatcher } from "iterator-matcher";
+
+// Import Internal Dependencies
+import { concatBinaryExpression } from "../src/index.js";
+import { codeToAst, getExpressionFromStatement, createTracer } from "./utils.js";
+
+test("given a BinaryExpression of two literals then the iterable must return Literal values", (tape) => {
+ const [astNode] = codeToAst("'foo' + 'bar' + 'xd'");
+ const iter = concatBinaryExpression(getExpressionFromStatement(astNode));
+
+ const iterResult = new IteratorMatcher()
+ .expect("foo")
+ .expect("bar")
+ .expect("xd")
+ .execute(iter, { allowNoMatchingValues: false });
+
+ tape.strictEqual(iterResult.isMatching, true);
+ tape.strictEqual(iterResult.elapsedSteps, 3);
+ tape.end();
+});
+
+test("given a BinaryExpression of two ArrayExpression then the iterable must return Array values as string", (tape) => {
+ const [astNode] = codeToAst("['A'] + ['B']");
+ const iter = concatBinaryExpression(getExpressionFromStatement(astNode));
+
+ const iterResult = new IteratorMatcher()
+ .expect("A")
+ .expect("B")
+ .execute(iter, { allowNoMatchingValues: false });
+
+ tape.strictEqual(iterResult.isMatching, true);
+ tape.strictEqual(iterResult.elapsedSteps, 2);
+ tape.end();
+});
+
+test("given a BinaryExpression of two Identifiers then the iterable must the tracer values", (tape) => {
+ const { tracer } = createTracer();
+ tracer.literalIdentifiers.set("foo", "A");
+ tracer.literalIdentifiers.set("bar", "B");
+
+ const [astNode] = codeToAst("foo + bar");
+ const iter = concatBinaryExpression(getExpressionFromStatement(astNode), { tracer });
+
+ const iterResult = new IteratorMatcher()
+ .expect("A")
+ .expect("B")
+ .execute(iter, { allowNoMatchingValues: false });
+
+ tape.strictEqual(iterResult.isMatching, true);
+ tape.strictEqual(iterResult.elapsedSteps, 2);
+ tape.end();
+});
+
+test("given a one level BinaryExpression with an unsupported node it should throw an Error", (tape) => {
+ tape.plan(1);
+ const { tracer } = createTracer();
+
+ const [astNode] = codeToAst("evil() + 's'");
+ try {
+ const iter = concatBinaryExpression(getExpressionFromStatement(astNode), {
+ tracer,
+ stopOnUnsupportedNode: true
+ });
+ iter.next();
+ }
+ catch (error) {
+ tape.strictEqual(error.message, "concatBinaryExpression:: Unsupported node detected");
+ }
+
+ tape.end();
+});
+
+test("given a Deep BinaryExpression with an unsupported node it should throw an Error", (tape) => {
+ tape.plan(1);
+ const { tracer } = createTracer();
+
+ const [astNode] = codeToAst("'a' + evil() + 's'");
+ try {
+ const iter = concatBinaryExpression(getExpressionFromStatement(astNode), {
+ tracer,
+ stopOnUnsupportedNode: true
+ });
+ iter.next();
+ }
+ catch (error) {
+ tape.strictEqual(error.message, "concatBinaryExpression:: Unsupported node detected");
+ }
+
+ tape.end();
+});
diff --git a/workspaces/estree-ast-utils/test/getCallExpressionIdentifier.spec.js b/workspaces/estree-ast-utils/test/getCallExpressionIdentifier.spec.js
new file mode 100644
index 0000000..e0d29ec
--- /dev/null
+++ b/workspaces/estree-ast-utils/test/getCallExpressionIdentifier.spec.js
@@ -0,0 +1,30 @@
+// Import Third-party Dependencies
+import test from "tape";
+
+// Import Internal Dependencies
+import { getCallExpressionIdentifier } from "../src/index.js";
+import { codeToAst, getExpressionFromStatement } from "./utils.js";
+
+test("given a JavaScript eval CallExpression then it must return eval", (tape) => {
+ const [astNode] = codeToAst("eval(\"this\");");
+ const nodeIdentifier = getCallExpressionIdentifier(getExpressionFromStatement(astNode));
+
+ tape.strictEqual(nodeIdentifier, "eval");
+ tape.end();
+});
+
+test("given a JavaScript Function() CallExpression then it must return Function", (tape) => {
+ const [astNode] = codeToAst("Function(\"return this\")();");
+ const nodeIdentifier = getCallExpressionIdentifier(getExpressionFromStatement(astNode));
+
+ tape.strictEqual(nodeIdentifier, "Function");
+ tape.end();
+});
+
+test("given a JavaScript AssignmentExpression then it must return null", (tape) => {
+ const [astNode] = codeToAst("foo = 10;");
+ const nodeIdentifier = getCallExpressionIdentifier(getExpressionFromStatement(astNode));
+
+ tape.strictEqual(nodeIdentifier, null);
+ tape.end();
+});
diff --git a/workspaces/estree-ast-utils/test/getMemberExpressionIdentifier.spec.js b/workspaces/estree-ast-utils/test/getMemberExpressionIdentifier.spec.js
new file mode 100644
index 0000000..8761e78
--- /dev/null
+++ b/workspaces/estree-ast-utils/test/getMemberExpressionIdentifier.spec.js
@@ -0,0 +1,84 @@
+// Import Third-party Dependencies
+import test from "tape";
+import { IteratorMatcher } from "iterator-matcher";
+
+// Import Internal Dependencies
+import { getMemberExpressionIdentifier } from "../src/index.js";
+import { codeToAst, createTracer, getExpressionFromStatement } from "./utils.js";
+
+test("it must return all literals part of the given MemberExpression", (tape) => {
+ const [astNode] = codeToAst("foo.bar.xd");
+ const iter = getMemberExpressionIdentifier(
+ getExpressionFromStatement(astNode)
+ );
+
+ const iterResult = new IteratorMatcher()
+ .expect("foo")
+ .expect("bar")
+ .expect("xd")
+ .execute(iter, { allowNoMatchingValues: false });
+
+ tape.strictEqual(iterResult.isMatching, true);
+ tape.strictEqual(iterResult.elapsedSteps, 3);
+ tape.end();
+});
+
+test("it must return all computed properties of the given MemberExpression", (tape) => {
+ const [astNode] = codeToAst("foo['bar']['xd']");
+ const iter = getMemberExpressionIdentifier(
+ getExpressionFromStatement(astNode)
+ );
+
+ const iterResult = new IteratorMatcher()
+ .expect("foo")
+ .expect("bar")
+ .expect("xd")
+ .execute(iter, { allowNoMatchingValues: false });
+
+ tape.strictEqual(iterResult.isMatching, true);
+ tape.strictEqual(iterResult.elapsedSteps, 3);
+
+ tape.end();
+});
+
+test(`given a MemberExpression with a computed property containing a deep tree of BinaryExpression
+ then it must return all literals parts even the last one which is the concatenation of the BinaryExpr`, (tape) => {
+ const [astNode] = codeToAst("foo.bar[\"k\" + \"e\" + \"y\"]");
+ const iter = getMemberExpressionIdentifier(
+ getExpressionFromStatement(astNode)
+ );
+
+ const iterResult = new IteratorMatcher()
+ .expect("foo")
+ .expect("bar")
+ .expect("key")
+ .execute(iter, { allowNoMatchingValues: false });
+
+ tape.strictEqual(iterResult.isMatching, true);
+ tape.strictEqual(iterResult.elapsedSteps, 3);
+
+ tape.end();
+});
+
+test(`given a MemberExpression with computed properties containing identifiers
+ then it must return all literals values from the tracer`, (tape) => {
+ const { tracer } = createTracer();
+ tracer.literalIdentifiers.set("foo", "hello");
+ tracer.literalIdentifiers.set("yo", "bar");
+
+ const [astNode] = codeToAst("hey[foo][yo]");
+ const iter = getMemberExpressionIdentifier(
+ getExpressionFromStatement(astNode), { tracer }
+ );
+
+ const iterResult = new IteratorMatcher()
+ .expect("hey")
+ .expect("hello")
+ .expect("bar")
+ .execute(iter, { allowNoMatchingValues: false });
+
+ tape.strictEqual(iterResult.isMatching, true);
+ tape.strictEqual(iterResult.elapsedSteps, 3);
+
+ tape.end();
+});
diff --git a/workspaces/estree-ast-utils/test/isLiteralRegex.spec.js b/workspaces/estree-ast-utils/test/isLiteralRegex.spec.js
new file mode 100644
index 0000000..97dd7be
--- /dev/null
+++ b/workspaces/estree-ast-utils/test/isLiteralRegex.spec.js
@@ -0,0 +1,22 @@
+// Import Third-party Dependencies
+import test from "tape";
+
+// Import Internal Dependencies
+import { isLiteralRegex } from "../src/index.js";
+import { codeToAst, getExpressionFromStatement } from "./utils.js";
+
+test("given a Literal Regex Node it should return true", (tape) => {
+ const [astNode] = codeToAst("/^a/g");
+ const isLRegex = isLiteralRegex(getExpressionFromStatement(astNode));
+
+ tape.strictEqual(isLRegex, true);
+ tape.end();
+});
+
+test("given a RegexObject Node it should return false", (tape) => {
+ const [astNode] = codeToAst("new RegExp('^hello')");
+ const isLRegex = isLiteralRegex(getExpressionFromStatement(astNode));
+
+ tape.strictEqual(isLRegex, false);
+ tape.end();
+});
diff --git a/workspaces/estree-ast-utils/test/utils.js b/workspaces/estree-ast-utils/test/utils.js
new file mode 100644
index 0000000..4d654f5
--- /dev/null
+++ b/workspaces/estree-ast-utils/test/utils.js
@@ -0,0 +1,62 @@
+// Import Third-party Dependencies
+import * as meriyah from "meriyah";
+import { walk } from "estree-walker";
+
+// Import Internal Dependencies
+import { VariableTracer } from "../src/index.js";
+
+export function codeToAst(code) {
+ const estreeRootNode = meriyah.parseScript(code, {
+ next: true,
+ loc: true,
+ raw: true,
+ module: true,
+ globalReturn: false
+ });
+
+ return estreeRootNode.body;
+}
+
+export function getExpressionFromStatement(node) {
+ return node.type === "ExpressionStatement" ? node.expression : null;
+}
+
+export function createTracer(enableDefaultTracing = false) {
+ const tracer = new VariableTracer();
+ if (enableDefaultTracing) {
+ tracer.enableDefaultTracing();
+ }
+
+ return {
+ tracer,
+ walkOnAst(astNode) {
+ walk(astNode, {
+ enter(node) {
+ tracer.walk(node);
+ }
+ });
+ },
+ /**
+ * @param {!string} codeStr
+ * @param {object} [options]
+ * @param {boolean} [options.debugAst=false]
+ * @returns {void}
+ */
+ walkOnCode(codeStr, options = {}) {
+ const { debugAst = false } = options;
+
+ const astNode = codeToAst(codeStr);
+ if (debugAst) {
+ console.log(JSON.stringify(astNode, null, 2));
+ }
+
+ this.walkOnAst(astNode);
+ },
+ getAssignmentArray(event = VariableTracer.AssignmentEvent) {
+ const assignmentEvents = [];
+ tracer.on(event, (value) => assignmentEvents.push(value));
+
+ return assignmentEvents;
+ }
+ };
+}
diff --git a/workspaces/estree-ast-utils/test/utils/getSubMemberExpressionSegments.spec.js b/workspaces/estree-ast-utils/test/utils/getSubMemberExpressionSegments.spec.js
new file mode 100644
index 0000000..bab8c93
--- /dev/null
+++ b/workspaces/estree-ast-utils/test/utils/getSubMemberExpressionSegments.spec.js
@@ -0,0 +1,19 @@
+// Import Third-party Dependencies
+import test from "tape";
+import { IteratorMatcher } from "iterator-matcher";
+
+// Import Internal Dependencies
+import { getSubMemberExpressionSegments } from "../../src/utils/index.js";
+
+test("given a MemberExpression then it should return each segments (except the last one)", (tape) => {
+ const iter = getSubMemberExpressionSegments("foo.bar.xd");
+
+ const iterResult = new IteratorMatcher()
+ .expect("foo")
+ .expect("foo.bar")
+ .execute(iter, { allowNoMatchingValues: false });
+
+ tape.strictEqual(iterResult.isMatching, true);
+ tape.strictEqual(iterResult.elapsedSteps, 2);
+ tape.end();
+});
diff --git a/workspaces/estree-ast-utils/test/utils/isEvilIdentifierPath.spec.js b/workspaces/estree-ast-utils/test/utils/isEvilIdentifierPath.spec.js
new file mode 100644
index 0000000..9affdf9
--- /dev/null
+++ b/workspaces/estree-ast-utils/test/utils/isEvilIdentifierPath.spec.js
@@ -0,0 +1,28 @@
+// Import Third-party Dependencies
+import test from "tape";
+
+// Import Internal Dependencies
+import { isEvilIdentifierPath } from "../../src/utils/index.js";
+
+test("given a random prototype method name then it should return false", (tape) => {
+ const result = isEvilIdentifierPath(
+ "Function.prototype.foo"
+ );
+
+ tape.strictEqual(result, false);
+
+ tape.end();
+});
+
+test("given a list of evil identifiers it should always return true", (tape) => {
+ const evilIdentifiers = [
+ "Function.prototype.bind",
+ "Function.prototype.call",
+ "Function.prototype.apply"
+ ];
+ for (const identifier of evilIdentifiers) {
+ tape.strictEqual(isEvilIdentifierPath(identifier), true);
+ }
+
+ tape.end();
+});
diff --git a/workspaces/estree-ast-utils/test/utils/notNullOrUndefined.spec.js b/workspaces/estree-ast-utils/test/utils/notNullOrUndefined.spec.js
new file mode 100644
index 0000000..4f7c031
--- /dev/null
+++ b/workspaces/estree-ast-utils/test/utils/notNullOrUndefined.spec.js
@@ -0,0 +1,21 @@
+// Import Third-party Dependencies
+import test from "tape";
+
+// Import Internal Dependencies
+import { notNullOrUndefined } from "../../src/utils/index.js";
+
+test("given a null or undefined primitive value then it must always return false", (tape) => {
+ tape.strictEqual(notNullOrUndefined(null), false, "null primitive value should return false");
+ tape.strictEqual(notNullOrUndefined(void 0), false, "undefined primitive value should return false");
+
+ tape.end();
+});
+
+test("given values (primitive or objects) that are not null or undefined then it must always return true", (tape) => {
+ const valuesToAssert = ["", 1, true, Symbol("foo"), {}, [], /^xd/g];
+ for (const value of valuesToAssert) {
+ tape.strictEqual(notNullOrUndefined(value), true);
+ }
+
+ tape.end();
+});
diff --git a/workspaces/sec-literal/LICENSE b/workspaces/sec-literal/LICENSE
new file mode 100644
index 0000000..346097d
--- /dev/null
+++ b/workspaces/sec-literal/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 NodeSecure
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/workspaces/sec-literal/README.md b/workspaces/sec-literal/README.md
new file mode 100644
index 0000000..d2e7552
--- /dev/null
+++ b/workspaces/sec-literal/README.md
@@ -0,0 +1,69 @@
+# Sec-literal
+![version](https://img.shields.io/badge/dynamic/json.svg?url=https://raw.githubusercontent.com/NodeSecure/js-x-ray/blob/master/workspaces/sec-literal/master/package.json&query=$.version&label=Version)
+[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/NodeSecure/js-x-ray/blob/master/workspaces/sec-literal/commit-activity)
+[![OpenSSF
+Scorecard](https://api.securityscorecards.dev/projects/github.com/NodeSecure/js-x-ray/blob/master/workspaces/sec-literal/badge)](https://api.securityscorecards.dev/projects/github.com/NodeSecure/js-x-ray/blob/master/workspaces/sec-literal)
+[![mit](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://github.com/NodeSecure/js-x-ray/blob/master/workspaces/sec-literal/blob/master/LICENSE)
+![build](https://img.shields.io/github/actions/workflow/status/NodeSecure/js-x-ray/blob/master/workspaces/sec-literal/node.js.yml)
+
+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).
+
+## Features
+
+- Detect Hexadecimal, Base64, Hexa and Unicode sequences.
+- Detect patterns (prefix, suffix) on groups of identifiers.
+- Detect suspicious string and return advanced metrics on it (char diversity etc).
+
+## Getting Started
+
+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).
+
+```bash
+$ npm i @nodesecure/sec-literal
+# or
+$ yarn add @nodesecure/sec-literal
+```
+
+## API
+
+## Hex
+
+### isHex(anyValue): boolean
+Detect if the given string is an Hexadecimal value
+
+### isSafe(anyValue): boolean
+Detect if the given string is a safe Hexadecimal value. The goal of this method is to eliminate false-positive.
+
+```js
+Hex.isSafe("1234"); // true
+Hex.isSafe("abcdef"); // true
+```
+
+## Literal
+
+### isLiteral(anyValue): boolean
+### toValue(anyValue): string
+### toRaw(anyValue): string
+### defaultAnalysis(literalValue)
+
+## Utils
+
+### isSvg(strValue): boolean
+
+### isSvgPath(strValue): boolean
+Detect if a given string is a svg path or not.
+
+### stringCharDiversity(str): number
+Get the number of unique chars in a given string
+
+### stringSuspicionScore(str): number
+Analyze a given string an give it a suspicion score (higher than 1 or 2 mean that the string is highly suspect).
+
+## Patterns
+
+### commonStringPrefix(leftStr, rightStr): string | null
+### commonStringSuffix(leftStr, rightStr): string | null
+### commonHexadecimalPrefix(identifiersArray: string[])
+
+## License
+MIT
diff --git a/workspaces/sec-literal/package.json b/workspaces/sec-literal/package.json
new file mode 100644
index 0000000..53b0995
--- /dev/null
+++ b/workspaces/sec-literal/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@nodesecure/sec-literal",
+ "version": "1.2.0",
+ "description": "Package created to analyze JavaScript literals",
+ "exports": "./src/index.js",
+ "private": false,
+ "type": "module",
+ "scripts": {
+ "lint": "eslint --ext .js",
+ "test-only": "cross-env esm-tape-runner 'test/*.spec.js' | tap-monkey",
+ "test": "cross-env npm run lint && npm run test-only"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/NodeSecure/sec-literal.git"
+ },
+ "keywords": [
+ "security",
+ "literal",
+ "estree",
+ "analysis",
+ "scanner"
+ ],
+ "files": [
+ "src"
+ ],
+ "author": "GENTILHOMME Thomas ",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/NodeSecure/sec-literal/issues"
+ },
+ "homepage": "https://github.com/NodeSecure/sec-literal#readme",
+ "dependencies": {
+ "frequency-set": "^1.0.2",
+ "is-base64": "^1.1.0",
+ "is-svg": "^4.3.2",
+ "string-width": "^5.1.2"
+ }
+}
diff --git a/workspaces/sec-literal/src/hex.js b/workspaces/sec-literal/src/hex.js
new file mode 100644
index 0000000..cb0988c
--- /dev/null
+++ b/workspaces/sec-literal/src/hex.js
@@ -0,0 +1,53 @@
+// Import Internal Dependencies
+import * as Literal from "./literal.js";
+import * as Utils from "./utils.js";
+
+const kUnsafeHexValues = new Set([
+ "require",
+ "length"
+].map((value) => Buffer.from(value).toString("hex")));
+
+// CONSTANTS
+const kSafeHexValues = new Set([
+ "0123456789",
+ "123456789",
+ "abcdef",
+ "abc123456789",
+ "0123456789abcdef",
+ "abcdef0123456789abcdef"
+]);
+
+export const CONSTANTS = Object.freeze({
+ SAFE_HEXA_VALUES: [...kSafeHexValues],
+ UNSAFE_HEXA_VALUES: [...kUnsafeHexValues]
+});
+
+/**
+ * @description detect if the given string is an Hexadecimal value
+ * @param {SecLiteral.Literal | string} anyValue
+ * @returns {boolean}
+ */
+export function isHex(anyValue) {
+ const value = Literal.toValue(anyValue);
+
+ return typeof value === "string" && /^[0-9A-Fa-f]{4,}$/g.test(value);
+}
+
+/**
+ * @description detect if the given string is a safe Hexadecimal value
+ * @param {SecLiteral.Literal | string} anyValue
+ * @returns {boolean}
+ */
+export function isSafe(anyValue) {
+ const rawValue = Literal.toRaw(anyValue);
+ if (kUnsafeHexValues.has(rawValue)) {
+ return false;
+ }
+
+ const charCount = Utils.stringCharDiversity(rawValue);
+ if (/^([0-9]+|[a-z]+|[A-Z]+)$/g.test(rawValue) || rawValue.length <= 5 || charCount <= 2) {
+ return true;
+ }
+
+ return [...kSafeHexValues].some((value) => rawValue.toLowerCase().startsWith(value));
+}
diff --git a/workspaces/sec-literal/src/index.js b/workspaces/sec-literal/src/index.js
new file mode 100644
index 0000000..8eaea83
--- /dev/null
+++ b/workspaces/sec-literal/src/index.js
@@ -0,0 +1,4 @@
+export * as Hex from "./hex.js";
+export * as Literal from "./literal.js";
+export * as Utils from "./utils.js";
+export * as Patterns from "./patterns.js";
diff --git a/workspaces/sec-literal/src/literal.js b/workspaces/sec-literal/src/literal.js
new file mode 100644
index 0000000..1937c07
--- /dev/null
+++ b/workspaces/sec-literal/src/literal.js
@@ -0,0 +1,43 @@
+// Import Third-party Dependencies
+import isStringBase64 from "is-base64";
+
+/**
+ * @param {SecLiteral.Literal | string} anyValue
+ * @returns {string}
+ */
+export function isLiteral(anyValue) {
+ return typeof anyValue === "object" && "type" in anyValue && anyValue.type === "Literal";
+}
+
+/**
+ * @param {SecLiteral.Literal | string} strOrLiteral
+ * @returns {string}
+ */
+export function toValue(strOrLiteral) {
+ return isLiteral(strOrLiteral) ? strOrLiteral.value : strOrLiteral;
+}
+
+/**
+ * @param {SecLiteral.Literal | string} strOrLiteral
+ * @returns {string}
+ */
+export function toRaw(strOrLiteral) {
+ return isLiteral(strOrLiteral) ? strOrLiteral.raw : strOrLiteral;
+}
+
+/**
+ * @param {!SecLiteral.Literal} literalValue
+ * @returns {SecLiteral.LiteralDefaultAnalysis}
+ */
+export function defaultAnalysis(literalValue) {
+ if (!isLiteral(literalValue)) {
+ return null;
+ }
+
+ const hasRawValue = "raw" in literalValue;
+ const hasHexadecimalSequence = hasRawValue ? /\\x[a-fA-F0-9]{2}/g.exec(literalValue.raw) !== null : null;
+ const hasUnicodeSequence = hasRawValue ? /\\u[a-fA-F0-9]{4}/g.exec(literalValue.raw) !== null : null;
+ const isBase64 = isStringBase64(literalValue.value, { allowEmpty: false });
+
+ return { hasHexadecimalSequence, hasUnicodeSequence, isBase64 };
+}
diff --git a/workspaces/sec-literal/src/patterns.js b/workspaces/sec-literal/src/patterns.js
new file mode 100644
index 0000000..034654c
--- /dev/null
+++ b/workspaces/sec-literal/src/patterns.js
@@ -0,0 +1,98 @@
+// Import Third-party Dependencies
+import FrequencySet from "frequency-set";
+
+// Import Internal Dependencies
+import * as Literal from "./literal.js";
+
+/**
+ * @description get the common string prefix (at the start) pattern
+ * @param {!string | SecLiteral} leftAnyValue
+ * @param {!string | SecLiteral} rightAnyValue
+ * @returns {string | null}
+ *
+ * @example
+ * commonStringPrefix("boo", "foo"); // null
+ * commonStringPrefix("bromance", "brother"); // "bro"
+ */
+export function commonStringPrefix(leftAnyValue, rightAnyValue) {
+ const leftStr = Literal.toValue(leftAnyValue);
+ const rightStr = Literal.toValue(rightAnyValue);
+
+ // The length of leftStr cannot be greater than that rightStr
+ const minLen = leftStr.length > rightStr.length ? rightStr.length : leftStr.length;
+ let commonStr = "";
+
+ for (let id = 0; id < minLen; id++) {
+ if (leftStr.charAt(id) !== rightStr.charAt(id)) {
+ break;
+ }
+
+ commonStr += leftStr.charAt(id);
+ }
+
+ return commonStr === "" ? null : commonStr;
+}
+
+function reverseString(string) {
+ return string.split("").reverse().join("");
+}
+
+/**
+ * @description get the common string suffixes (at the end) pattern
+ * @param {!string} leftStr
+ * @param {!string} rightStr
+ * @returns {string | null}
+ *
+ * @example
+ * commonStringSuffix("boo", "foo"); // oo
+ * commonStringSuffix("bromance", "brother"); // null
+ */
+export function commonStringSuffix(leftStr, rightStr) {
+ const commonPrefix = commonStringPrefix(
+ reverseString(leftStr),
+ reverseString(rightStr)
+ );
+
+ return commonPrefix === null ? null : reverseString(commonPrefix);
+}
+
+export function commonHexadecimalPrefix(identifiersArray) {
+ if (!Array.isArray(identifiersArray)) {
+ throw new TypeError("identifiersArray must be an Array");
+ }
+ const prefix = new FrequencySet();
+
+ mainLoop: for (const value of identifiersArray.slice().sort()) {
+ for (const [cp, count] of prefix) {
+ const commonStr = commonStringPrefix(value, cp);
+ if (commonStr === null) {
+ continue;
+ }
+
+ if (commonStr === cp || commonStr.startsWith(cp)) {
+ prefix.add(cp);
+ }
+ else if (cp.startsWith(commonStr)) {
+ prefix.delete(cp);
+ prefix.add(commonStr, count + 1);
+ }
+ continue mainLoop;
+ }
+
+ prefix.add(value);
+ }
+
+ // We remove one-time occurences (because they are normal variables)
+ let oneTimeOccurence = 0;
+ for (const [key, value] of prefix.entries()) {
+ if (value === 1) {
+ prefix.delete(key);
+ oneTimeOccurence++;
+ }
+ }
+
+ return {
+ oneTimeOccurence,
+ prefix: Object.fromEntries(prefix)
+ };
+}
diff --git a/workspaces/sec-literal/src/utils.js b/workspaces/sec-literal/src/utils.js
new file mode 100644
index 0000000..e77be2d
--- /dev/null
+++ b/workspaces/sec-literal/src/utils.js
@@ -0,0 +1,93 @@
+// Import Third-party Dependencies
+import isStringSvg from "is-svg";
+import stringWidth from "string-width";
+
+// Import Internal Dependencies
+import { toValue } from "./literal.js";
+
+/**
+ * @param {SecLiteral.Literal | string} strOrLiteral
+ * @returns {boolean}
+ */
+export function isSvg(strOrLiteral) {
+ try {
+ const value = toValue(strOrLiteral);
+
+ return isStringSvg(value) || isSvgPath(value);
+ }
+ catch {
+ return false;
+ }
+}
+
+/**
+ * @description detect if a given string is a svg path or not.
+ * @param {!string} str svg path literal
+ * @returns {boolean}
+ */
+export function isSvgPath(str) {
+ if (typeof str !== "string") {
+ return false;
+ }
+ const trimStr = str.trim();
+
+ return trimStr.length > 4 && /^[mzlhvcsqta]\s*[-+.0-9][^mlhvzcsqta]+/i.test(trimStr) && /[\dz]$/i.test(trimStr);
+}
+
+/**
+ * @description detect if a given string is a morse value.
+ * @param {!string} str any string value
+ * @returns {boolean}
+ */
+export function isMorse(str) {
+ return /^[.-]{1,5}(?:[\s\t]+[.-]{1,5})*(?:[\s\t]+[.-]{1,5}(?:[\s\t]+[.-]{1,5})*)*$/g.test(str);
+}
+
+/**
+ * @param {!string} str any string value
+ * @returns {string}
+ */
+export function escapeRegExp(str) {
+ return str.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
+}
+
+/**
+ * @description Get the number of unique chars in a given string
+ * @param {!string} str string
+ * @param {string[]} [charsToExclude=[]]
+ * @returns {number}
+ */
+export function stringCharDiversity(str, charsToExclude = []) {
+ const data = new Set(str);
+ charsToExclude.forEach((char) => data.delete(char));
+
+ return data.size;
+}
+
+// ---
+const kMaxSafeStringLen = 45;
+const kMaxSafeStringCharDiversity = 70;
+const kMinUnsafeStringLenThreshold = 200;
+const kScoreStringLengthThreshold = 750;
+
+/**
+ * @description Analyze a given string an give it a suspicion score (higher than 1 or 2 mean that the string is highly suspect).
+ * @param {!string} str string to analyze
+ * @returns {number}
+ */
+export function stringSuspicionScore(str) {
+ const strLen = stringWidth(str);
+ if (strLen < kMaxSafeStringLen) {
+ return 0;
+ }
+
+ const includeSpace = str.includes(" ");
+ const includeSpaceAtStart = includeSpace ? str.slice(0, kMaxSafeStringLen).includes(" ") : false;
+
+ let suspectScore = includeSpaceAtStart ? 0 : 1;
+ if (strLen > kMinUnsafeStringLenThreshold) {
+ suspectScore += Math.ceil(strLen / kScoreStringLengthThreshold);
+ }
+
+ return stringCharDiversity(str) >= kMaxSafeStringCharDiversity ? suspectScore + 2 : suspectScore;
+}
diff --git a/workspaces/sec-literal/test/hex.spec.js b/workspaces/sec-literal/test/hex.spec.js
new file mode 100644
index 0000000..fb1b4e9
--- /dev/null
+++ b/workspaces/sec-literal/test/hex.spec.js
@@ -0,0 +1,77 @@
+// Import Node.js Dependencies
+import { randomBytes } from "node:crypto";
+
+// Import Third-party Dependencies
+import test from "tape";
+
+// Import Internal Dependencies
+import { isHex, isSafe, CONSTANTS } from "../src/hex.js";
+import { createLiteral } from "./utils/index.js";
+
+test("isHex() of a random Hexadecimal value must return true", (tape) => {
+ const hexValue = randomBytes(4).toString("hex");
+
+ tape.strictEqual(isHex(hexValue), true, `Hexadecimal value '${hexValue}' must return true`);
+ tape.end();
+});
+
+test("isHex() of an ESTree Literal containing a random Hexadecimal value must return true", (tape) => {
+ const hexValue = createLiteral(randomBytes(4).toString("hex"));
+
+ tape.strictEqual(isHex(hexValue), true, `Hexadecimal value '${hexValue.value}' must return true`);
+ tape.end();
+});
+
+test("An hexadecimal value must be at least 4 chars long", (tape) => {
+ const hexValue = randomBytes(1).toString("hex");
+
+ tape.strictEqual(isHex(hexValue), false, `Hexadecimal value '${hexValue}' must return false`);
+ tape.end();
+});
+
+test("isHex() of a value that is not a string or an ESTree Literal must return false", (tape) => {
+ const hexValue = 100;
+
+ tape.strictEqual(isHex(hexValue), false, "100 is typeof number so it must always return false");
+ tape.end();
+});
+
+test("isSafe must return true for a value with a length lower or equal five characters", (tape) => {
+ tape.ok(isSafe("h2l5x"));
+ tape.end();
+});
+
+test("isSafe must return true if the string diversity is only two characters or lower", (tape) => {
+ tape.ok(isSafe("aaaaaaaaaaaaaabbbbbbbbbbbbb"));
+ tape.end();
+});
+
+test("isSafe must always return true if argument is only number, lower or upper letters", (tape) => {
+ const values = ["00000000", "aaaaaaaa", "AAAAAAAAA"];
+
+ for (const hexValue of values) {
+ tape.ok(isSafe(hexValue));
+ }
+ tape.end();
+});
+
+test("isSafe() must always return true if the value start with one of the 'safe' values", (tape) => {
+ for (const safeValue of CONSTANTS.SAFE_HEXA_VALUES) {
+ const hexValue = safeValue + randomBytes(4).toString("hex");
+
+ tape.ok(isSafe(hexValue));
+ }
+ tape.end();
+});
+
+test("isSafe must return true because it start with a safe pattern (and it must lowerCase the string)", (tape) => {
+ tape.ok(isSafe("ABCDEF1234567890"));
+ tape.end();
+});
+
+test("isSafe() must always return false if the value start with one of the 'unsafe' values", (tape) => {
+ for (const unsafeValue of CONSTANTS.UNSAFE_HEXA_VALUES) {
+ tape.strictEqual(isSafe(unsafeValue), false);
+ }
+ tape.end();
+});
diff --git a/workspaces/sec-literal/test/literal.spec.js b/workspaces/sec-literal/test/literal.spec.js
new file mode 100644
index 0000000..b47df12
--- /dev/null
+++ b/workspaces/sec-literal/test/literal.spec.js
@@ -0,0 +1,99 @@
+// Import Node.js Dependencies
+import { randomBytes } from "node:crypto";
+
+// Import Third-party Dependencies
+import test from "tape";
+
+// Import Internal Dependencies
+import { isLiteral, toValue, toRaw, defaultAnalysis } from "../src/literal.js";
+import { createLiteral } from "./utils/index.js";
+
+test("isLiteral must return true for a valid ESTree Literal Node", (tape) => {
+ const literalSample = createLiteral("boo");
+
+ tape.strictEqual(isLiteral(literalSample), true);
+ tape.strictEqual(isLiteral("hey"), false);
+ tape.strictEqual(isLiteral({ type: "fake", value: "boo" }), false);
+ tape.end();
+});
+
+test("toValue must return a string when we give a valid EStree Literal", (tape) => {
+ const literalSample = createLiteral("boo");
+
+ tape.strictEqual(toValue(literalSample), "boo");
+ tape.strictEqual(toValue("hey"), "hey");
+ tape.end();
+});
+
+test("toRaw must return a string when we give a valid EStree Literal", (tape) => {
+ const literalSample = createLiteral("boo", true);
+
+ tape.strictEqual(toRaw(literalSample), "boo");
+ tape.strictEqual(toRaw("hey"), "hey");
+ tape.end();
+});
+
+test("defaultAnalysis() of something else than a Literal must always return null", (tape) => {
+ tape.strictEqual(defaultAnalysis(10), null);
+ tape.end();
+});
+
+test("defaultAnalysis() of an Hexadecimal value", (tape) => {
+ const hexValue = randomBytes(10).toString("hex");
+
+ const result = defaultAnalysis(createLiteral(hexValue, true));
+ const expected = {
+ isBase64: true, hasHexadecimalSequence: false, hasUnicodeSequence: false
+ };
+
+ tape.deepEqual(result, expected);
+ tape.end();
+});
+
+test("defaultAnalysis() of an Base64 value", (tape) => {
+ const hexValue = randomBytes(10).toString("base64");
+
+ const result = defaultAnalysis(createLiteral(hexValue, true));
+ const expected = {
+ isBase64: true, hasHexadecimalSequence: false, hasUnicodeSequence: false
+ };
+
+ tape.deepEqual(result, expected);
+ tape.end();
+});
+
+test("defaultAnalysis() of an Unicode Sequence", (tape) => {
+ const unicodeSequence = createLiteral("'\\u0024\\u0024'", true);
+
+ const result = defaultAnalysis(unicodeSequence);
+ const expected = {
+ isBase64: false, hasHexadecimalSequence: false, hasUnicodeSequence: true
+ };
+
+ tape.deepEqual(result, expected);
+ tape.end();
+});
+
+test("defaultAnalysis() of an Unicode Sequence", (tape) => {
+ const hexSequence = createLiteral("'\\x64\\x61\\x74\\x61'", true);
+
+ const result = defaultAnalysis(hexSequence);
+ const expected = {
+ isBase64: false, hasHexadecimalSequence: true, hasUnicodeSequence: false
+ };
+
+ tape.deepEqual(result, expected);
+ tape.end();
+});
+
+test("defaultAnalysis() with a Literal with no 'raw' property must return two null values", (tape) => {
+ const hexValue = randomBytes(10).toString("base64");
+
+ const result = defaultAnalysis(createLiteral(hexValue));
+ const expected = {
+ isBase64: true, hasHexadecimalSequence: null, hasUnicodeSequence: null
+ };
+
+ tape.deepEqual(result, expected);
+ tape.end();
+});
diff --git a/workspaces/sec-literal/test/patterns.spec.js b/workspaces/sec-literal/test/patterns.spec.js
new file mode 100644
index 0000000..89db85b
--- /dev/null
+++ b/workspaces/sec-literal/test/patterns.spec.js
@@ -0,0 +1,64 @@
+// Import Internal Dependencies
+import {
+ commonStringPrefix,
+ commonStringSuffix,
+ commonHexadecimalPrefix
+} from "../src/patterns.js";
+
+// Import Third-party Dependencies
+import test from "tape";
+
+test("commonStringPrefix of two strings that does not start with the same set of characters must return null", (tape) => {
+ tape.strictEqual(commonStringPrefix("boo", "foo"), null,
+ "there is no common prefix between 'boo' and 'foo' so the result must be null");
+ tape.end();
+});
+
+test("commonStringPrefix of two strings that start with the same set of characters must return it as result", (tape) => {
+ tape.strictEqual(commonStringPrefix("bromance", "brother"), "bro",
+ "the common prefix between bromance and brother must be 'bro'.");
+ tape.end();
+});
+
+test("commonStringSuffix of two strings that end with the same set of characters must return it as result", (tape) => {
+ tape.strictEqual(commonStringSuffix("boo", "foo"), "oo",
+ "the common suffix between boo and foo must be 'oo'");
+ tape.end();
+});
+
+test("commonStringSuffix of two strings that does not end with the same set of characters must return null", (tape) => {
+ tape.strictEqual(commonStringSuffix("bromance", "brother"), null,
+ "there is no common suffix between 'bromance' and 'brother' so the result must be null");
+ tape.end();
+});
+
+test("commonHexadecimalPrefix - throw a TypeError if identifiersArray is not an Array", (tape) => {
+ tape.throws(() => commonHexadecimalPrefix(10), "identifiersArray must be an Array");
+ tape.end();
+});
+
+test("commonHexadecimalPrefix - only hexadecimal identifiers", (tape) => {
+ const data = [
+ "_0x3c0c55", "_0x1185d5", "_0x160fc8", "_0x18a66f", "_0x18a835", "_0x1a8356",
+ "_0x1adf3b", "_0x1e4510", "_0x1e9a2a", "_0x215558", "_0x2b0194", "_0x2fffe5",
+ "_0x32c822", "_0x33bb79"
+ ];
+ const result = commonHexadecimalPrefix(data);
+
+ tape.strictEqual(result.oneTimeOccurence, 0);
+ tape.strictEqual(result.prefix._0x, data.length);
+ tape.end();
+});
+
+test("commonHexadecimalPrefix - add one non-hexadecimal identifier", (tape) => {
+ const data = [
+ "_0x3c0c55", "_0x1185d5", "_0x160fc8", "_0x18a66f", "_0x18a835", "_0x1a8356",
+ "_0x1adf3b", "_0x1e4510", "_0x1e9a2a", "_0x215558", "_0x2b0194", "_0x2fffe5",
+ "_0x32c822", "_0x33bb79", "foo"
+ ];
+ const result = commonHexadecimalPrefix(data);
+
+ tape.strictEqual(result.oneTimeOccurence, 1);
+ tape.strictEqual(result.prefix._0x, data.length - 1);
+ tape.end();
+});
diff --git a/workspaces/sec-literal/test/utils.spec.js b/workspaces/sec-literal/test/utils.spec.js
new file mode 100644
index 0000000..830dfe4
--- /dev/null
+++ b/workspaces/sec-literal/test/utils.spec.js
@@ -0,0 +1,89 @@
+/* eslint-disable max-len */
+
+// Import Node.js Dependencies
+import { randomBytes } from "node:crypto";
+
+// Import Third-party Dependencies
+import test from "tape";
+
+// Import Internal Dependencies
+import { stringCharDiversity, isSvg, isSvgPath, stringSuspicionScore } from "../src/utils.js";
+
+test("stringCharDiversity must return the number of unique chars in a given string", (tape) => {
+ tape.strictEqual(stringCharDiversity("helloo!"), 5,
+ "the following string 'helloo!' contains five unique chars: h, e, l, o and !");
+ tape.end();
+});
+
+test("stringCharDiversity must return the number of unique chars in a given string (but with exclusions of given chars)", (tape) => {
+ tape.strictEqual(stringCharDiversity("- - -\n", ["\n"]), 2);
+ tape.end();
+});
+
+test("isSvg must return true for an HTML svg balise", (tape) => {
+ const SVGHTML = ``;
+ tape.strictEqual(isSvg(SVGHTML), true);
+ tape.end();
+});
+
+test("isSvg of a SVG Path must return true", (tape) => {
+ tape.strictEqual(isSvg("M150 0 L75 200 L225 200 Z"), true);
+ tape.end();
+});
+
+test("isSvg must return false for invalid XML string", (tape) => {
+ tape.strictEqual(isSvg(""), false);
+ tape.end();
+});
+
+test("isSvgPath must return true when we give a valid svg path and false when the string is not valid", (tape) => {
+ tape.strictEqual(isSvgPath("M150 0 L75 200 L225 200 Z"), true);
+ tape.strictEqual(isSvgPath("M150"), false, "the length of an svg path must be always higher than four characters");
+ tape.strictEqual(isSvgPath("hello world!"), false);
+ tape.strictEqual(isSvgPath(10), false, "isSvgPath argument must always return false for anything that is not a string primitive");
+ tape.end();
+});
+
+test("stringSuspicionScore must always return 0 if the string length if below 45", (tape) => {
+ for (let strSize = 1; strSize < 45; strSize++) {
+ // We generate a random String (with slice it in two because a size of 20 for hex is 40 bytes).
+ const randomStr = randomBytes(strSize).toString("hex").slice(strSize);
+
+ tape.strictEqual(stringSuspicionScore(randomStr), 0);
+ }
+ tape.end();
+});
+
+test("stringSuspicionScore must return one if the str is between 45 and 200 chars and had no space in the first 45 chars", (tape) => {
+ const randomStrWithNoSpaces = randomBytes(25).toString("hex");
+
+ tape.strictEqual(stringSuspicionScore(randomStrWithNoSpaces), 1);
+ tape.end();
+});
+
+test("stringSuspicionScore must return zero if the str is between 45 and 200 chars and has at least one space in the first 45 chars", (tape) => {
+ const randomStrWithSpaces = randomBytes(10).toString("hex") + " -_- " + randomBytes(30).toString("hex");
+
+ tape.strictEqual(stringSuspicionScore(randomStrWithSpaces), 0);
+ tape.end();
+});
+
+test("stringSuspicionScore must return a score of two for a string with more than 200 chars and no spaces", (tape) => {
+ const randomStr = randomBytes(200).toString("hex");
+
+ tape.strictEqual(stringSuspicionScore(randomStr), 2);
+ tape.end();
+});
+
+test("stringSuspicionScore must add two to the final score when the string has more than 70 uniques chars", (tape) => {
+ const randomStr = "૱꠸┯┰┱┲❗►◄Ăă0123456789ᶀᶁᶂᶃᶄᶆᶇᶈᶉᶊᶋᶌᶍᶎᶏᶐᶑᶒᶓᶔᶕᶖᶗᶘᶙᶚᶸᵯᵰᵴᵶᵹᵼᵽᵾᵿ⤢⤣⤤⤥⥆⥇™°×π±√ ";
+
+ tape.strictEqual(stringSuspicionScore(randomStr), 3);
+ tape.end();
+});
diff --git a/workspaces/sec-literal/test/utils/index.js b/workspaces/sec-literal/test/utils/index.js
new file mode 100644
index 0000000..afc0950
--- /dev/null
+++ b/workspaces/sec-literal/test/utils/index.js
@@ -0,0 +1,10 @@
+// @see https://github.com/estree/estree/blob/master/es5.md#literal
+export function createLiteral(value, includeRaw = false) {
+ const node = { type: "Literal", value };
+ if (includeRaw) {
+ node.raw = value;
+ }
+
+ return node;
+}
+