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/poor-jars-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nodesecure/js-x-ray": minor
---

Move trojan-source detection from SourceFile to AstAnalyser
9 changes: 8 additions & 1 deletion workspaces/js-x-ray/src/AstAnalyser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { isOneLineExpressionExport } from "./utils/index.js";
import { JsSourceParser, type SourceParser } from "./JsSourceParser.js";
import { ProbeRunner, type Probe } from "./ProbeRunner.js";
import * as trojan from "./obfuscators/trojan-source.js";

export interface Dependency {
unsafe: boolean;
Expand Down Expand Up @@ -143,7 +144,13 @@ export class AstAnalyser {
const body = this.parser.parse(this.prepareSource(str, { removeHTMLComments }), {
isEcmaScriptModule: Boolean(module)
});
const source = new SourceFile(str);
const source = new SourceFile();
if (trojan.verify(str)) {
source.warnings.push(
generateWarning("obfuscated-code", { value: "trojan-source" })
);
}

const runner = new ProbeRunner(source, this.probes);

if (initialize) {
Expand Down
12 changes: 0 additions & 12 deletions workspaces/js-x-ray/src/SourceFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
} from "./warnings.js";
import type { Dependency } from "./AstAnalyser.js";
import { Deobfuscator } from "./Deobfuscator.js";
import * as trojan from "./obfuscators/trojan-source.js";

// CONSTANTS
const kMaximumEncodedLiterals = 10;
Expand All @@ -30,17 +29,6 @@ export class SourceFile {
encodedLiterals = new Map<string, string>();
warnings: Warning[] = [];
flags = new Set<SourceFlags>();

constructor(
sourceCodeString: string
) {
if (trojan.verify(sourceCodeString)) {
this.warnings.push(
generateWarning("obfuscated-code", { value: "trojan-source" })
);
}
}

addDependency(
name: string,
location?: ESTree.SourceLocation | null,
Expand Down
18 changes: 9 additions & 9 deletions workspaces/js-x-ray/test/ProbeRunner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ 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(new SourceFile(""));
const pr = new ProbeRunner(new SourceFile());

assert.strictEqual(pr.probes, ProbeRunner.Defaults);
});
Expand All @@ -30,7 +30,7 @@ describe("ProbeRunner", () => {
];

// @ts-expect-error
const pr = new ProbeRunner(new SourceFile(""), fakeProbe);
const pr = new ProbeRunner(new SourceFile(), fakeProbe);
assert.strictEqual(pr.probes, fakeProbe);
});

Expand All @@ -44,7 +44,7 @@ describe("ProbeRunner", () => {
];

// @ts-expect-error
const pr = new ProbeRunner(new SourceFile(""), fakeProbe);
const pr = new ProbeRunner(new SourceFile(), fakeProbe);
assert.strictEqual(pr.probes, fakeProbe);
});

Expand All @@ -56,7 +56,7 @@ describe("ProbeRunner", () => {

function instantiateProbeRunner() {
// @ts-expect-error
return new ProbeRunner(new SourceFile(""), [fakeProbe]);
return new ProbeRunner(new SourceFile(), [fakeProbe]);
}

assert.throws(instantiateProbeRunner, Error, "Invalid probe");
Expand All @@ -70,7 +70,7 @@ describe("ProbeRunner", () => {

function instantiateProbeRunner() {
// @ts-expect-error
return new ProbeRunner(new SourceFile(""), [fakeProbe]);
return new ProbeRunner(new SourceFile(), [fakeProbe]);
}

assert.throws(instantiateProbeRunner, Error, "Invalid probe");
Expand All @@ -85,7 +85,7 @@ describe("ProbeRunner", () => {

function instantiateProbeRunner() {
// @ts-expect-error
return new ProbeRunner(new SourceFile(""), [fakeProbe]);
return new ProbeRunner(new SourceFile(), [fakeProbe]);
}

assert.throws(instantiateProbeRunner, Error, "Invalid probe");
Expand All @@ -94,7 +94,7 @@ describe("ProbeRunner", () => {

describe("walk", () => {
it("should pass validateNode, enter main and then teardown", () => {
const sourceFile = new SourceFile("");
const sourceFile = new SourceFile();
const fakeProbe = {
validateNode: (node: ESTree.Node) => [node.type === "Literal"],
main: mock.fn(),
Expand Down Expand Up @@ -130,7 +130,7 @@ describe("ProbeRunner", () => {
};

const pr = new ProbeRunner(
new SourceFile(""),
new SourceFile(),
// @ts-expect-error
[fakeProbe]
);
Expand Down Expand Up @@ -170,7 +170,7 @@ describe("ProbeRunner", () => {

const probes = [fakeProbe, fakeProbeBreak, fakeProbeSkip];

const sourceFile = new SourceFile("");
const sourceFile = new SourceFile();

const pr = new ProbeRunner(
sourceFile,
Expand Down
6 changes: 3 additions & 3 deletions workspaces/js-x-ray/test/probes/isArrayExpression.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ test("it should trigger analyzeLiteral method one time", (t) => {
const str = "['foo']";

const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isArrayExpression);
const sastAnalysis = getSastAnalysis(isArrayExpression);

const analyzeLiteralMock = t.mock.method(sastAnalysis.sourceFile, "analyzeLiteral");
sastAnalysis.execute(ast.body);
Expand All @@ -28,7 +28,7 @@ test("it should trigger analyzeLiteral method two times (ignoring the holey betw
const str = "[5, ,10]";

const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isArrayExpression);
const sastAnalysis = getSastAnalysis(isArrayExpression);

const analyzeLiteralMock = t.mock.method(sastAnalysis.sourceFile, "analyzeLiteral");
sastAnalysis.execute(ast.body);
Expand All @@ -43,7 +43,7 @@ test("it should trigger analyzeLiteral one time (ignoring non-literal Node)", (t
const str = "[5, () => void 0]";

const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isArrayExpression);
const sastAnalysis = getSastAnalysis(isArrayExpression);

const analyzeLiteralMock = t.mock.method(sastAnalysis.sourceFile, "analyzeLiteral");
sastAnalysis.execute(ast.body);
Expand Down
4 changes: 2 additions & 2 deletions workspaces/js-x-ray/test/probes/isBinaryExpression.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import isBinaryExpression from "../../src/probes/isBinaryExpression.js";
test("should detect 1 deep binary expression", () => {
const str = "0x1*-0x12df+-0x1fb9*-0x1+0x2*-0x66d";
const ast = parseScript(str);
const { sourceFile } = getSastAnalysis(str, isBinaryExpression)
const { sourceFile } = getSastAnalysis(isBinaryExpression)
.execute(ast.body);

assert.equal(sourceFile.deobfuscator.deepBinaryExpression, 1);
Expand All @@ -18,7 +18,7 @@ test("should detect 1 deep binary expression", () => {
test("should not detect deep binary expression", () => {
const str = "10 + 5 - (10)";
const ast = parseScript(str);
const { sourceFile } = getSastAnalysis(str, isBinaryExpression)
const { sourceFile } = getSastAnalysis(isBinaryExpression)
.execute(ast.body);

assert.equal(sourceFile.deobfuscator.deepBinaryExpression, 0);
Expand Down
12 changes: 6 additions & 6 deletions workspaces/js-x-ray/test/probes/isImportDeclaration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import isImportDeclaration from "../../src/probes/isImportDeclaration.js";
test("should detect 1 dependency for an ImportNamespaceSpecifier", () => {
const str = "import * as foo from \"bar\"";
const ast = parseScript(str);
const { sourceFile } = getSastAnalysis(str, isImportDeclaration)
const { sourceFile } = getSastAnalysis(isImportDeclaration)
.execute(ast.body);

const { dependencies } = sourceFile;
Expand All @@ -19,7 +19,7 @@ test("should detect 1 dependency for an ImportNamespaceSpecifier", () => {
test("should detect 1 dependency for an ImportDefaultSpecifier", () => {
const str = "import foo from \"bar\"";
const ast = parseScript(str);
const { sourceFile } = getSastAnalysis(str, isImportDeclaration)
const { sourceFile } = getSastAnalysis(isImportDeclaration)
.execute(ast.body);

const { dependencies } = sourceFile;
Expand All @@ -29,7 +29,7 @@ test("should detect 1 dependency for an ImportDefaultSpecifier", () => {
test("should detect 1 dependency for an ImportSpecifier", () => {
const str = "import { xd } from \"bar\"";
const ast = parseScript(str);
const { sourceFile } = getSastAnalysis(str, isImportDeclaration)
const { sourceFile } = getSastAnalysis(isImportDeclaration)
.execute(ast.body);

const { dependencies } = sourceFile;
Expand All @@ -39,7 +39,7 @@ test("should detect 1 dependency for an ImportSpecifier", () => {
test("should detect 1 dependency with no specificiers", () => {
const str = "import \"bar\"";
const ast = parseScript(str);
const { sourceFile } = getSastAnalysis(str, isImportDeclaration)
const { sourceFile } = getSastAnalysis(isImportDeclaration)
.execute(ast.body);

const { dependencies } = sourceFile;
Expand All @@ -49,7 +49,7 @@ test("should detect 1 dependency with no specificiers", () => {
test("should detect 1 dependency for an ImportExpression", () => {
const str = "import(\"bar\")";
const ast = parseScript(str);
const { sourceFile } = getSastAnalysis(str, isImportDeclaration)
const { sourceFile } = getSastAnalysis(isImportDeclaration)
.execute(ast.body);

const { dependencies } = sourceFile;
Expand All @@ -66,7 +66,7 @@ test("should detect an unsafe import using data:text/javascript and throw a unsa

importNodes.forEach((str) => {
const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isImportDeclaration)
const sastAnalysis = getSastAnalysis(isImportDeclaration)
.execute(ast.body);

assert.strictEqual(sastAnalysis.warnings().length, 1);
Expand Down
34 changes: 17 additions & 17 deletions workspaces/js-x-ray/test/probes/isLiteral.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ test("should throw an unsafe-import because the hexadecimal string is equal to t
const str = "const foo = '68747470'";
const ast = parseScript(str);

const sastAnalysis = getSastAnalysis(str, isLiteral);
const sastAnalysis = getSastAnalysis(isLiteral);
const analyzeStringMock = t.mock.method(sastAnalysis.sourceFile.deobfuscator, "analyzeString");
sastAnalysis.execute(ast.body);

Expand All @@ -28,7 +28,7 @@ test("should throw an encoded-literal warning because the hexadecimal value is e
const str = "const _t = globalThis['72657175697265']";
const ast = parseScript(str);

const sastAnalysis = getSastAnalysis(str, isLiteral);
const sastAnalysis = getSastAnalysis(isLiteral);
const analyzeStringMock = t.mock.method(sastAnalysis.sourceFile.deobfuscator, "analyzeString");
sastAnalysis.execute(ast.body);

Expand All @@ -44,7 +44,7 @@ test("should throw an encoded-literal warning because the hexadecimal value is e
test("should not throw an encoded-literal warning because hexadecimal value is safe", () => {
const str = "const foo = '123456789'";
const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isLiteral)
const sastAnalysis = getSastAnalysis(isLiteral)
.execute(ast.body);

assert.strictEqual(sastAnalysis.warnings().length, 0);
Expand All @@ -54,7 +54,7 @@ test("should throw an encoded-literal warning because hexadecimal value is not s
// Note: hexadecimal equal 'hello world'
const str = "const foo = '68656c6c6f20776f726c64'";
const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isLiteral)
const sastAnalysis = getSastAnalysis(isLiteral)
.execute(ast.body);

assert.strictEqual(sastAnalysis.warnings().length, 1);
Expand All @@ -66,7 +66,7 @@ test("should not throw any warnings without hexadecimal value (and should call a
const str = "const foo = 'hello world!'";
const ast = parseScript(str);

const sastAnalysis = getSastAnalysis(str, isLiteral);
const sastAnalysis = getSastAnalysis(isLiteral);
const analyzeLiteralMock = t.mock.method(sastAnalysis.sourceFile, "analyzeLiteral");
sastAnalysis.execute(ast.body);

Expand All @@ -81,7 +81,7 @@ test("should not throw any warnings without hexadecimal value (and should call a
test("should detect shady link when an URL is bit.ly", () => {
const str = "const foo = 'http://bit.ly/foo'";
const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isLiteral).execute(ast.body);
const sastAnalysis = getSastAnalysis(isLiteral).execute(ast.body);

assert.strictEqual(sastAnalysis.warnings().length, 1);
const warning = sastAnalysis.getWarning("shady-link");
Expand All @@ -91,7 +91,7 @@ test("should detect shady link when an URL is bit.ly", () => {
test("should detect shady link when an URL is ipinfo.io when protocol is http", () => {
const str = "const foo = 'http://ipinfo.io/json'";
const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isLiteral).execute(ast.body);
const sastAnalysis = getSastAnalysis(isLiteral).execute(ast.body);
assert.strictEqual(sastAnalysis.warnings().length, 1);
const warning = sastAnalysis.getWarning("shady-link");
assert.strictEqual(warning.value, "http://ipinfo.io/json");
Expand All @@ -100,7 +100,7 @@ test("should detect shady link when an URL is ipinfo.io when protocol is http",
test("should detect shady link when an URL is ipinfo.io when protocol is https", () => {
const str = "const foo = 'https://ipinfo.io/json'";
const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isLiteral).execute(ast.body);
const sastAnalysis = getSastAnalysis(isLiteral).execute(ast.body);
assert.strictEqual(sastAnalysis.warnings().length, 1);
const warning = sastAnalysis.getWarning("shady-link");
assert.strictEqual(warning.value, "https://ipinfo.io/json");
Expand All @@ -109,7 +109,7 @@ test("should detect shady link when an URL is ipinfo.io when protocol is https",
test("should detect shady link when an URL is httpbin.org when protocol is http", () => {
const str = "const foo = 'http://httpbin.org/ip'";
const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isLiteral).execute(ast.body);
const sastAnalysis = getSastAnalysis(isLiteral).execute(ast.body);
assert.strictEqual(sastAnalysis.warnings().length, 1);
const warning = sastAnalysis.getWarning("shady-link");
assert.strictEqual(warning.value, "http://httpbin.org/ip");
Expand All @@ -118,7 +118,7 @@ test("should detect shady link when an URL is httpbin.org when protocol is http"
test("should detect shady link when an URL is httpbin.org when protocol is https", () => {
const str = "const foo = 'https://httpbin.org/ip'";
const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isLiteral).execute(ast.body);
const sastAnalysis = getSastAnalysis(isLiteral).execute(ast.body);
assert.strictEqual(sastAnalysis.warnings().length, 1);
const warning = sastAnalysis.getWarning("shady-link");
assert.strictEqual(warning.value, "https://httpbin.org/ip");
Expand All @@ -127,7 +127,7 @@ test("should detect shady link when an URL is httpbin.org when protocol is https
test("should detect shady link when an URL has a suspicious domain", () => {
const str = "const foo = 'http://foobar.link'";
const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isLiteral).execute(ast.body);
const sastAnalysis = getSastAnalysis(isLiteral).execute(ast.body);

assert.strictEqual(sastAnalysis.warnings().length, 1);
const warning = sastAnalysis.getWarning("shady-link");
Expand All @@ -137,23 +137,23 @@ test("should detect shady link when an URL has a suspicious domain", () => {
test("should not mark suspicious links the IPv4 address range 127.0.0.0/8 (localhost 127.0.0.1)", () => {
const str = "const IPv4URL = ['http://127.0.0.1/script', 'http://127.7.7.7/script']";
const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isLiteral).execute(ast.body);
const sastAnalysis = getSastAnalysis(isLiteral).execute(ast.body);

assert.ok(!sastAnalysis.warnings().length);
});

test("should not be considered suspicious a link with a raw IPv4 address 127.0.0.1 and a port", () => {
const str = "const IPv4URL = 'http://127.0.0.1:80/script'";
const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isLiteral).execute(ast.body);
const sastAnalysis = getSastAnalysis(isLiteral).execute(ast.body);

assert.ok(!sastAnalysis.warnings().length);
});

test("should detect the link as suspicious when a URL contains a raw IPv4 address", () => {
const str = "const IPv4URL = 'http://77.244.210.247/burpcollaborator.txt'";
const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isLiteral).execute(ast.body);
const sastAnalysis = getSastAnalysis(isLiteral).execute(ast.body);

assert.strictEqual(sastAnalysis.warnings().length, 1);
const warning = sastAnalysis.getWarning("shady-link");
Expand All @@ -163,7 +163,7 @@ test("should detect the link as suspicious when a URL contains a raw IPv4 addres
test("should detect suspicious links when a URL contains a raw IPv4 address with port", () => {
const str = "const IPv4URL = 'http://77.244.210.247:8080/script'";
const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isLiteral).execute(ast.body);
const sastAnalysis = getSastAnalysis(isLiteral).execute(ast.body);

assert.strictEqual(sastAnalysis.warnings().length, 1);
const warning = sastAnalysis.getWarning("shady-link");
Expand All @@ -173,7 +173,7 @@ test("should detect suspicious links when a URL contains a raw IPv4 address with
test("should detect suspicious links when a URL contains a raw IPv6 address", () => {
const str = "const IPv6URL = 'http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]/index.html'";
const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isLiteral).execute(ast.body);
const sastAnalysis = getSastAnalysis(isLiteral).execute(ast.body);

assert.strictEqual(sastAnalysis.warnings().length, 1);
const warning = sastAnalysis.getWarning("shady-link");
Expand All @@ -183,7 +183,7 @@ test("should detect suspicious links when a URL contains a raw IPv6 address", ()
test("should detect suspicious links when a URL contains a raw IPv6 address with port", () => {
const str = "const IPv6URL = 'http://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:100/script'";
const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isLiteral).execute(ast.body);
const sastAnalysis = getSastAnalysis(isLiteral).execute(ast.body);

assert.strictEqual(sastAnalysis.warnings().length, 1);
const warning = sastAnalysis.getWarning("shady-link");
Expand Down
2 changes: 1 addition & 1 deletion workspaces/js-x-ray/test/probes/isLiteralRegex.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import isLiteralRegex from "../../src/probes/isLiteralRegex.js";
test("should throw a 'unsafe-regex' warning because the given RegExp Literal is unsafe", () => {
const str = "const foo = /(a+){10}/g;";
const ast = parseScript(str);
const sastAnalysis = getSastAnalysis(str, isLiteralRegex)
const sastAnalysis = getSastAnalysis(isLiteralRegex)
.execute(ast.body);

assert.strictEqual(sastAnalysis.warnings().length, 1);
Expand Down
Loading