From 3f9a1fa718775b08b1346e97ffa78bd1abad2618 Mon Sep 17 00:00:00 2001 From: cgombauld Date: Sat, 28 Jun 2025 19:50:49 +0200 Subject: [PATCH 1/3] feat(probes): add initialize --- .changeset/four-bars-bet.md | 5 +++ workspaces/js-x-ray/docs/AstAnalyser.md | 2 +- workspaces/js-x-ray/src/ProbeRunner.js | 8 ++++ workspaces/js-x-ray/src/SourceFile.js | 35 ----------------- workspaces/js-x-ray/src/probes/isSyncIO.js | 39 +++++++++++++++++++ .../js-x-ray/src/probes/isWeakCrypto.js | 8 ++++ workspaces/js-x-ray/src/types/api.d.ts | 1 + workspaces/js-x-ray/test/ProbeRunner.spec.js | 14 +++++++ 8 files changed, 76 insertions(+), 36 deletions(-) create mode 100644 .changeset/four-bars-bet.md diff --git a/.changeset/four-bars-bet.md b/.changeset/four-bars-bet.md new file mode 100644 index 00000000..22d23090 --- /dev/null +++ b/.changeset/four-bars-bet.md @@ -0,0 +1,5 @@ +--- +"@nodesecure/js-x-ray": minor +--- + +feat(probes): add initialize diff --git a/workspaces/js-x-ray/docs/AstAnalyser.md b/workspaces/js-x-ray/docs/AstAnalyser.md index f9452eb3..b3a0d12c 100644 --- a/workspaces/js-x-ray/docs/AstAnalyser.md +++ b/workspaces/js-x-ray/docs/AstAnalyser.md @@ -134,7 +134,7 @@ scanner.analyse("const foo = 'bar';", { You can also create custom probes to detect specific pattern in the code you are analyzing. -A probe is a pair of two functions (`validateNode` and `main`) that will be called on each node of the AST. It will return a warning if the pattern is detected. +A probe is a pair of two required functions (`validateNode` and `main`) that will be called on each node of the AST and one optional function (`initialize`) that will be call before walking the AST.It will return a warning if the pattern is detected. Below a basic probe that detect a string assignation to `danger`: diff --git a/workspaces/js-x-ray/src/ProbeRunner.js b/workspaces/js-x-ray/src/ProbeRunner.js index 5f88d000..1cbfdc54 100644 --- a/workspaces/js-x-ray/src/ProbeRunner.js +++ b/workspaces/js-x-ray/src/ProbeRunner.js @@ -28,6 +28,7 @@ import isSyncIO from "./probes/isSyncIO.js"; * @property {(options: any) => void} teardown * @property {boolean} [breakOnMatch=false] * @property {string} [breakGroup] + * @property {(sourceFile: SourceFile) => void} [initialize] */ export const ProbeSignals = Object.freeze({ @@ -85,6 +86,13 @@ export class ProbeRunner { typeof probe.main === "function", `Invalid probe ${probe.name}: main must be a function` ); + assert( + typeof probe.initialize === "function" || probe.initialize === undefined, + `Invalid probe ${probe.name}: initialize must be a function or undefined` + ); + if (probe.initialize) { + probe.initialize(sourceFile); + } } this.probes = probes; diff --git a/workspaces/js-x-ray/src/SourceFile.js b/workspaces/js-x-ray/src/SourceFile.js index 193cb187..3324ec36 100644 --- a/workspaces/js-x-ray/src/SourceFile.js +++ b/workspaces/js-x-ray/src/SourceFile.js @@ -11,36 +11,6 @@ import * as trojan from "./obfuscators/trojan-source.js"; // CONSTANTS const kMaximumEncodedLiterals = 10; -const kIdentifierOrMemberExps = [ - "crypto.createHash", - "crypto.pbkdf2Sync", - "crypto.scryptSync", - "crypto.generateKeyPairSync", - "fs.readFileSync", - "fs.writeFileSync", - "fs.appendFileSync", - "fs.readSync", - "fs.writeSync", - "fs.readdirSync", - "fs.statSync", - "fs.mkdirSync", - "fs.renameSync", - "fs.unlinkSync", - "fs.symlinkSync", - "fs.openSync", - "fs.fstatSync", - "fs.linkSync", - "fs.realpathSync", - "child_process.execSync", - "child_process.spawnSync", - "child_process.execFileSync", - "zlib.deflateSync", - "zlib.inflateSync", - "zlib.gzipSync", - "zlib.gunzipSync", - "zlib.brotliCompressSync", - "zlib.brotliDecompressSync" -]; export class SourceFile { inTryStatement = false; @@ -56,11 +26,6 @@ export class SourceFile { this.tracer = new VariableTracer() .enableDefaultTracing(); - kIdentifierOrMemberExps.forEach((identifierOrMemberExp) => this.tracer.trace(identifierOrMemberExp, { - followConsecutiveAssignment: true, - moduleName: identifierOrMemberExp.split(".")[0] - })); - let probes = ProbeRunner.Defaults; if (Array.isArray(probesOptions.customProbes) && probesOptions.customProbes.length > 0) { probes = probesOptions.skipDefaultProbes === true ? probesOptions.customProbes : [...probes, ...probesOptions.customProbes]; diff --git a/workspaces/js-x-ray/src/probes/isSyncIO.js b/workspaces/js-x-ray/src/probes/isSyncIO.js index e2d05618..ff61d5af 100644 --- a/workspaces/js-x-ray/src/probes/isSyncIO.js +++ b/workspaces/js-x-ray/src/probes/isSyncIO.js @@ -3,6 +3,35 @@ import { getCallExpressionIdentifier } from "@nodesecure/estree-ast-utils"; // Constants const kTracedNodeCoreModules = ["fs", "crypto", "child_process", "zlib"]; +const kSyncIOIdentifierOrMemberExps = [ + "crypto.pbkdf2Sync", + "crypto.scryptSync", + "crypto.generateKeyPairSync", + "fs.readFileSync", + "fs.writeFileSync", + "fs.appendFileSync", + "fs.readSync", + "fs.writeSync", + "fs.readdirSync", + "fs.statSync", + "fs.mkdirSync", + "fs.renameSync", + "fs.unlinkSync", + "fs.symlinkSync", + "fs.openSync", + "fs.fstatSync", + "fs.linkSync", + "fs.realpathSync", + "child_process.execSync", + "child_process.spawnSync", + "child_process.execFileSync", + "zlib.deflateSync", + "zlib.inflateSync", + "zlib.gzipSync", + "zlib.gunzipSync", + "zlib.brotliCompressSync", + "zlib.brotliDecompressSync" +]; function validateNode(node, { tracer }) { const id = getCallExpressionIdentifier(node, { tracer }); @@ -15,6 +44,15 @@ function validateNode(node, { tracer }) { return [data !== null && data.identifierOrMemberExpr.endsWith("Sync")]; } +function initialize(sourceFile) { + if (sourceFile) { + kSyncIOIdentifierOrMemberExps.forEach((identifierOrMemberExp) => sourceFile.tracer.trace(identifierOrMemberExp, { + followConsecutiveAssignment: true, + moduleName: identifierOrMemberExp.split(".")[0] + })); + } +} + function main(node, { sourceFile }) { sourceFile.addWarning("synchronous-io", node.callee.name, node.loc); } @@ -23,5 +61,6 @@ export default { name: "isSyncIO", validateNode, main, + initialize, breakOnMatch: false }; diff --git a/workspaces/js-x-ray/src/probes/isWeakCrypto.js b/workspaces/js-x-ray/src/probes/isWeakCrypto.js index 87384399..1c7824bf 100644 --- a/workspaces/js-x-ray/src/probes/isWeakCrypto.js +++ b/workspaces/js-x-ray/src/probes/isWeakCrypto.js @@ -21,6 +21,13 @@ function validateNode(node, { tracer }) { return [data !== null && data.identifierOrMemberExpr === "crypto.createHash"]; } +function initialize(sourceFile) { + sourceFile?.tracer.trace("crypto.createHash", { + followConsecutiveAssignment: true, + moduleName: "crypto" + }); +} + function main(node, { sourceFile }) { const arg = node.arguments.at(0); @@ -33,5 +40,6 @@ export default { name: "isWeakCrypto", validateNode, main, + initialize, breakOnMatch: false }; diff --git a/workspaces/js-x-ray/src/types/api.d.ts b/workspaces/js-x-ray/src/types/api.d.ts index 5bab680a..8b298fd3 100644 --- a/workspaces/js-x-ray/src/types/api.d.ts +++ b/workspaces/js-x-ray/src/types/api.d.ts @@ -94,6 +94,7 @@ interface AstAnalyserOptions { interface Probe { validateNode: Function | Function[]; main: Function; + initialize?: (sourceFile: SourceFile) => void; } interface Report { diff --git a/workspaces/js-x-ray/test/ProbeRunner.spec.js b/workspaces/js-x-ray/test/ProbeRunner.spec.js index c3c0aa46..f1cb456e 100644 --- a/workspaces/js-x-ray/test/ProbeRunner.spec.js +++ b/workspaces/js-x-ray/test/ProbeRunner.spec.js @@ -66,6 +66,20 @@ describe("ProbeRunner", () => { assert.throws(instantiateProbeRunner, Error, "Invalid probe"); }); + + it("should fail if initialize initialize is present and not a function", () => { + const fakeProbe = { + validateNode: mock.fn(), + main: mock.fn(), + initialize: null + }; + + function instantiateProbeRunner() { + return new ProbeRunner(null, [fakeProbe]); + } + + assert.throws(instantiateProbeRunner, Error, "Invalid probe"); + }); }); describe("walk", () => { From 25536bc5d5f9bd4a279649fbf6af2da6b788d17e Mon Sep 17 00:00:00 2001 From: cgombauld Date: Sun, 29 Jun 2025 10:41:19 +0200 Subject: [PATCH 2/3] refactor(probes) sourceFile can't ever be null --- workspaces/js-x-ray/src/probes/isSyncIO.js | 10 ++++------ workspaces/js-x-ray/src/probes/isWeakCrypto.js | 2 +- workspaces/js-x-ray/test/ProbeRunner.spec.js | 16 +++++++--------- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/workspaces/js-x-ray/src/probes/isSyncIO.js b/workspaces/js-x-ray/src/probes/isSyncIO.js index ff61d5af..c0b1b31f 100644 --- a/workspaces/js-x-ray/src/probes/isSyncIO.js +++ b/workspaces/js-x-ray/src/probes/isSyncIO.js @@ -45,12 +45,10 @@ function validateNode(node, { tracer }) { } function initialize(sourceFile) { - if (sourceFile) { - kSyncIOIdentifierOrMemberExps.forEach((identifierOrMemberExp) => sourceFile.tracer.trace(identifierOrMemberExp, { - followConsecutiveAssignment: true, - moduleName: identifierOrMemberExp.split(".")[0] - })); - } + kSyncIOIdentifierOrMemberExps.forEach((identifierOrMemberExp) => sourceFile.tracer.trace(identifierOrMemberExp, { + followConsecutiveAssignment: true, + moduleName: identifierOrMemberExp.split(".")[0] + })); } function main(node, { sourceFile }) { diff --git a/workspaces/js-x-ray/src/probes/isWeakCrypto.js b/workspaces/js-x-ray/src/probes/isWeakCrypto.js index 1c7824bf..44eccf76 100644 --- a/workspaces/js-x-ray/src/probes/isWeakCrypto.js +++ b/workspaces/js-x-ray/src/probes/isWeakCrypto.js @@ -22,7 +22,7 @@ function validateNode(node, { tracer }) { } function initialize(sourceFile) { - sourceFile?.tracer.trace("crypto.createHash", { + sourceFile.tracer.trace("crypto.createHash", { followConsecutiveAssignment: true, moduleName: "crypto" }); diff --git a/workspaces/js-x-ray/test/ProbeRunner.spec.js b/workspaces/js-x-ray/test/ProbeRunner.spec.js index f1cb456e..796a40ae 100644 --- a/workspaces/js-x-ray/test/ProbeRunner.spec.js +++ b/workspaces/js-x-ray/test/ProbeRunner.spec.js @@ -4,13 +4,13 @@ import assert from "node:assert"; // Import Internal Dependencies import { ProbeRunner, ProbeSignals } from "../src/ProbeRunner.js"; +import { SourceFile } from "../src/SourceFile.js"; describe("ProbeRunner", () => { describe("constructor", () => { it("should instanciate class with Defaults probes when none are provide", () => { - const pr = new ProbeRunner(null); + const pr = new ProbeRunner(new SourceFile("")); - assert.strictEqual(pr.sourceFile, null); assert.strictEqual(pr.probes, ProbeRunner.Defaults); }); @@ -23,8 +23,7 @@ describe("ProbeRunner", () => { } ]; - const pr = new ProbeRunner(null, fakeProbe); - assert.strictEqual(pr.sourceFile, null); + const pr = new ProbeRunner(new SourceFile(""), fakeProbe); assert.strictEqual(pr.probes, fakeProbe); }); @@ -36,8 +35,7 @@ describe("ProbeRunner", () => { teardown: mock.fn() }]; - const pr = new ProbeRunner(null, fakeProbe); - assert.strictEqual(pr.sourceFile, null); + const pr = new ProbeRunner(new SourceFile(""), fakeProbe); assert.strictEqual(pr.probes, fakeProbe); }); @@ -48,7 +46,7 @@ describe("ProbeRunner", () => { }; function instantiateProbeRunner() { - return new ProbeRunner(null, [fakeProbe]); + return new ProbeRunner(new SourceFile(""), [fakeProbe]); } assert.throws(instantiateProbeRunner, Error, "Invalid probe"); @@ -61,7 +59,7 @@ describe("ProbeRunner", () => { }; function instantiateProbeRunner() { - return new ProbeRunner(null, [fakeProbe]); + return new ProbeRunner(new SourceFile(""), [fakeProbe]); } assert.throws(instantiateProbeRunner, Error, "Invalid probe"); @@ -75,7 +73,7 @@ describe("ProbeRunner", () => { }; function instantiateProbeRunner() { - return new ProbeRunner(null, [fakeProbe]); + return new ProbeRunner(new SourceFile(""), [fakeProbe]); } assert.throws(instantiateProbeRunner, Error, "Invalid probe"); From 091eec56b1acddc76202916cca9402a108f99d5f Mon Sep 17 00:00:00 2001 From: Clement Gombauld Date: Sun, 29 Jun 2025 11:26:13 +0200 Subject: [PATCH 3/3] Update workspaces/js-x-ray/test/ProbeRunner.spec.js Co-authored-by: PierreDemailly <39910767+PierreDemailly@users.noreply.github.com> --- workspaces/js-x-ray/test/ProbeRunner.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspaces/js-x-ray/test/ProbeRunner.spec.js b/workspaces/js-x-ray/test/ProbeRunner.spec.js index 796a40ae..ed80a4dd 100644 --- a/workspaces/js-x-ray/test/ProbeRunner.spec.js +++ b/workspaces/js-x-ray/test/ProbeRunner.spec.js @@ -65,7 +65,7 @@ describe("ProbeRunner", () => { assert.throws(instantiateProbeRunner, Error, "Invalid probe"); }); - it("should fail if initialize initialize is present and not a function", () => { + it("should fail if initialize is present and not a function", () => { const fakeProbe = { validateNode: mock.fn(), main: mock.fn(),