Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-files-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nodesecure/js-x-ray": minor
---

Implement context for Probe and ProbeRunner
6 changes: 3 additions & 3 deletions workspaces/js-x-ray/docs/AstAnalyser.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,11 @@ Below a basic probe that detect a string assignation to `danger`:
export const customProbes = [
{
name: "customProbeUnsafeDanger",
validateNode: (node, sourceFile) => [
validateNode: (node) => [
node.type === "VariableDeclaration" && node.declarations[0].init.value === "danger"
],
main: (node, options) => {
const { sourceFile, data: calleeName } = options;
main: (node, ctx) => {
const { sourceFile, data: calleeName } = ctx;
if (node.declarations[0].init.value === "danger") {
sourceFile.warnings.push({
kind: "unsafe-danger",
Expand Down
63 changes: 39 additions & 24 deletions workspaces/js-x-ray/src/ProbeRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,29 @@ import type { SourceFile } from "./SourceFile.js";
import type { OptionalWarningName } from "./warnings.js";

export type ProbeReturn = void | null | symbol;
export type ProbeInitializeCallback = (sourceFile: SourceFile) => void;
export type ProbeFinalizeCallback = (sourceFile: SourceFile) => void;
export type ProbeMainCallback = (
node: any,
options: { sourceFile: SourceFile; data?: any; }
) => ProbeReturn;
export type ProbeTeardownCallback = (options: { sourceFile: SourceFile; }) => void;
export type ProbeValidationCallback = (node: ESTree.Node, sourceFile: SourceFile) => [boolean, any?];

export interface Probe {
export type ProbeContextDef = Record<string, any>;
export type ProbeContext<T extends ProbeContextDef = ProbeContextDef> = {
sourceFile: SourceFile;
context?: T;
};

export type ProbeValidationCallback<T extends ProbeContextDef = ProbeContextDef> = (
node: ESTree.Node, ctx: ProbeContext<T>
) => [boolean, any?];

export interface Probe<T extends ProbeContextDef = ProbeContextDef> {
name: string;
initialize?: ProbeInitializeCallback;
finalize?: ProbeFinalizeCallback;
validateNode: ProbeValidationCallback | ProbeValidationCallback[];
main: ProbeMainCallback;
teardown?: ProbeTeardownCallback;
initialize?: (ctx: ProbeContext<T>) => void | ProbeContext;
finalize?: (ctx: ProbeContext<T>) => void;
validateNode: ProbeValidationCallback<T> | ProbeValidationCallback<T>[];
main: (
node: any,
ctx: ProbeContext<T> & { data?: any; }
) => ProbeReturn;
teardown?: (ctx: ProbeContext<T>) => void;
breakOnMatch?: boolean;
breakGroup?: string;
context?: ProbeContext<T>;
}

export const ProbeSignals = Object.freeze({
Expand Down Expand Up @@ -97,28 +102,42 @@ export class ProbeRunner {
`Invalid probe ${probe.name}: initialize must be a function or undefined`
);
if (probe.initialize) {
probe.initialize(sourceFile);
const context = probe.initialize(this.#getProbeContext(probe));
if (context) {
probe.context = context;
}
}
}

this.probes = probes;
}

#getProbeContext(
probe: Probe
): ProbeContext {
return {
sourceFile: this.sourceFile,
context: probe.context
};
}

#runProbe(
probe: Probe,
node: ESTree.Node
): ProbeReturn {
const validationFns = Array.isArray(probe.validateNode) ?
probe.validateNode : [probe.validateNode];
const ctx = this.#getProbeContext(probe);

for (const validateNode of validationFns) {
const [isMatching, data = null] = validateNode(
node,
this.sourceFile
ctx
);

if (isMatching) {
return probe.main(node, {
sourceFile: this.sourceFile,
...ctx,
data
});
}
Expand Down Expand Up @@ -158,9 +177,7 @@ export class ProbeRunner {
}
}
finally {
if (probe.teardown) {
probe.teardown({ sourceFile: this.sourceFile });
}
probe.teardown?.(this.#getProbeContext(probe));
}
}

Expand All @@ -169,9 +186,7 @@ export class ProbeRunner {

finalize(): void {
for (const probe of this.probes) {
if (probe.finalize) {
probe.finalize(this.sourceFile);
}
probe.finalize?.(this.#getProbeContext(probe));
}
}
}
13 changes: 9 additions & 4 deletions workspaces/js-x-ray/src/probes/isFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { getCallExpressionIdentifier } from "@nodesecure/estree-ast-utils";
import type { ESTree } from "meriyah";

// Import Internal Dependencies
import { SourceFile } from "../SourceFile.js";
import type { ProbeContext } from "../ProbeRunner.js";

function validateNode(
node: ESTree.Node,
{ tracer }: SourceFile
ctx: ProbeContext
): [boolean, any?] {
const { tracer } = ctx.sourceFile;
const id = getCallExpressionIdentifier(node);

if (id === null) {
Expand All @@ -20,13 +21,17 @@ function validateNode(
return [data !== null && data.identifierOrMemberExpr === "fetch"];
}

function initialize(sourceFile: SourceFile) {
function initialize(
ctx: ProbeContext
) {
const { sourceFile } = ctx;

sourceFile.tracer.trace("fetch", { followConsecutiveAssignment: true });
}

function main(
_node: ESTree.Node,
{ sourceFile }: { sourceFile: SourceFile; }
{ sourceFile }: ProbeContext
) {
sourceFile.flags.add("fetch");
}
Expand Down
12 changes: 6 additions & 6 deletions workspaces/js-x-ray/src/probes/isRequire/isRequire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ import {
import type { ESTree } from "meriyah";

// Import Internal Dependencies
import { ProbeSignals } from "../../ProbeRunner.js";
import { SourceFile } from "../../SourceFile.js";
import { ProbeSignals, type ProbeContext } from "../../ProbeRunner.js";
import { isLiteral } from "../../types/estree.js";
import { RequireCallExpressionWalker } from "./RequireCallExpressionWalker.js";
import { generateWarning } from "../../warnings.js";

function validateNodeRequire(
node: ESTree.Node,
{ tracer }: SourceFile
ctx: ProbeContext
): [boolean, any?] {
const { tracer } = ctx.sourceFile;
const id = getCallExpressionIdentifier(node, {
resolveCallExpression: false
});
Expand Down Expand Up @@ -63,14 +63,14 @@ function validateNodeEvalRequire(
}

function teardown(
{ sourceFile }: { sourceFile: SourceFile; }
ctx: ProbeContext
) {
sourceFile.dependencyAutoWarning = false;
ctx.sourceFile.dependencyAutoWarning = false;
}

function main(
node: ESTree.CallExpression,
options: { sourceFile: SourceFile; data?: string; }
options: ProbeContext & { data?: string; }
) {
const { sourceFile, data: calleeName } = options;
const { tracer } = sourceFile;
Expand Down
15 changes: 9 additions & 6 deletions workspaces/js-x-ray/src/probes/isSerializeEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import {
import type { ESTree } from "meriyah";

// Import Internal Dependencies
import { SourceFile } from "../SourceFile.js";
import { generateWarning } from "../warnings.js";
import { ProbeSignals } from "../ProbeRunner.js";
import { ProbeSignals, type ProbeContext } from "../ProbeRunner.js";

/**
* @description Detect serialization of process.env which could indicate environment variable exfiltration
Expand All @@ -20,8 +19,10 @@ import { ProbeSignals } from "../ProbeRunner.js";
*/
function validateNode(
node: ESTree.Node,
{ tracer }: SourceFile
ctx: ProbeContext
): [boolean, any?] {
const { tracer } = ctx.sourceFile;

const id = getCallExpressionIdentifier(node);

if (id === null) {
Expand Down Expand Up @@ -58,9 +59,9 @@ function validateNode(

function main(
node: ESTree.Node,
options: { sourceFile: SourceFile; }
ctx: ProbeContext
) {
const { sourceFile } = options;
const { sourceFile } = ctx;

const warning = generateWarning("serialize-environment", {
value: "JSON.stringify(process.env)",
Expand All @@ -72,8 +73,10 @@ function main(
}

function initialize(
{ tracer }: SourceFile
ctx: ProbeContext
) {
const { tracer } = ctx.sourceFile;

tracer
.trace("process.env", {
followConsecutiveAssignment: true
Expand Down
13 changes: 7 additions & 6 deletions workspaces/js-x-ray/src/probes/isSyncIO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getCallExpressionIdentifier } from "@nodesecure/estree-ast-utils";
import type { ESTree } from "meriyah";

// Import Internal Dependencies
import { SourceFile } from "../SourceFile.js";
import type { ProbeContext } from "../ProbeRunner.js";
import { generateWarning } from "../warnings.js";

// CONSTANTS
Expand Down Expand Up @@ -40,8 +40,9 @@ const kSyncIOIdentifierOrMemberExps = [

function validateNode(
node: ESTree.Node,
{ tracer }: SourceFile
ctx: ProbeContext
): [boolean, any?] {
const { tracer } = ctx.sourceFile;
const id = getCallExpressionIdentifier(
node,
{
Expand All @@ -64,12 +65,12 @@ function validateNode(
}

function initialize(
sourceFile: SourceFile
ctx: ProbeContext
) {
kSyncIOIdentifierOrMemberExps.forEach((identifierOrMemberExp) => {
const moduleName = identifierOrMemberExp.split(".")[0];

return sourceFile.tracer.trace(identifierOrMemberExp, {
ctx.sourceFile.tracer.trace(identifierOrMemberExp, {
followConsecutiveAssignment: true,
moduleName
});
Expand All @@ -78,13 +79,13 @@ function initialize(

function main(
node: ESTree.CallExpression,
{ sourceFile }: { sourceFile: SourceFile; }
ctx: ProbeContext
) {
const warning = generateWarning("synchronous-io", {
value: node.callee.name,
location: node.loc
});
sourceFile.warnings.push(warning);
ctx.sourceFile.warnings.push(warning);
}

export default {
Expand Down
15 changes: 9 additions & 6 deletions workspaces/js-x-ray/src/probes/isWeakCrypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getCallExpressionIdentifier } from "@nodesecure/estree-ast-utils";
import type { ESTree } from "meriyah";

// Import Internal Dependencies
import { SourceFile } from "../SourceFile.js";
import type { ProbeContext } from "../ProbeRunner.js";
import { generateWarning } from "../warnings.js";
import {
isLiteral
Expand All @@ -20,9 +20,9 @@ const kWeakAlgorithms = new Set([

function validateNode(
node: ESTree.Node,
sourceFile: SourceFile
ctx: ProbeContext
): [boolean, any?] {
const { tracer } = sourceFile;
const { tracer } = ctx.sourceFile;

const id = getCallExpressionIdentifier(node);
if (id === null || !tracer.importedModules.has("crypto")) {
Expand All @@ -37,18 +37,21 @@ function validateNode(
}

function initialize(
sourceFile: SourceFile
ctx: ProbeContext
) {
sourceFile.tracer.trace("crypto.createHash", {
const { tracer } = ctx.sourceFile;

tracer.trace("crypto.createHash", {
followConsecutiveAssignment: true,
moduleName: "crypto"
});
}

function main(
node: ESTree.CallExpression,
{ sourceFile }: { sourceFile: SourceFile; }
ctx: ProbeContext
) {
const { sourceFile } = ctx;
const arg = node.arguments.at(0);

if (isLiteral(arg) && kWeakAlgorithms.has(arg.value)) {
Expand Down
Loading