Skip to content

Commit df7032e

Browse files
authored
feat(js-x-ray)! dependencies is now considered as collectables and is not in the report anymore (#532)
1 parent 5f69bf4 commit df7032e

25 files changed

Lines changed: 430 additions & 211 deletions

.changeset/short-jobs-visit.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nodesecure/js-x-ray": major
3+
---
4+
5+
feat(js-x-ray)! dependencies is now considered as collectables and is not in the report anymore

workspaces/js-x-ray/docs/AstAnalyser.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,11 @@ class AstAnalyser {
5555
) => Report;
5656
analyseFile(
5757
pathToFile: string | URL,
58-
options?: RuntimeFileOptions
58+
options?: RuntimeOptions
5959
): Promise<ReportOnFile>;
6060
analyseFileSync(
6161
pathToFile: string | URL,
62-
options?: RuntimeFileOptions
62+
options?: RuntimeOptions
6363
): ReportOnFile;
6464
prepareSource(source: string, options?: PrepareSourceOptions): string
6565
}
@@ -83,12 +83,12 @@ interface RuntimeOptions {
8383
isMinified?: boolean;
8484
initialize?: (sourceFile: SourceFile) => void;
8585
finalize?: (sourceFile: SourceFile) => void;
86+
packageName?: string;
8687
}
8788

8889
type SourceFlags = "fetch" | "oneline-require" | "is-minified";
8990

9091
interface Report {
91-
dependencies: Map<string, Dependency>;
9292
warnings: Warning[];
9393
flags: Set<SourceFlags>;
9494
idsLengthAvg: number;
@@ -99,7 +99,6 @@ type ReportOnFile = {
9999
ok: true,
100100
warnings: Warning[];
101101
flags: Set<SourceFlags>;
102-
dependencies: Map<string, Dependency>;
103102
} | {
104103
ok: false,
105104
warnings: Warning[];
@@ -208,7 +207,6 @@ Result:
208207
idsLengthAvg: 0,
209208
stringScore: 0,
210209
warnings: [ { kind: 'unsafe-danger', location: [Array], source: 'JS-X-Ray' } ],
211-
dependencies: Map(0) {},
212210
flags: Set(0) {},
213211
isOneLineRequire: false
214212
}

workspaces/js-x-ray/docs/CollectableSet.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# CollectableSet
22

3-
CollectableSet is a specialized data structure for collecting and aggregating infrastructure-related data points (e.g., URLs, hostnames, IPs) during JavaScript AST analysis. It groups locations by value and file, with optional metadata support. Post-analysis, the collected data can be exploited externally (e.g., for network monitoring, security audits, or infrastructure mapping) to derive insights beyond JS-X-Ray's built-in warnings.
3+
CollectableSet is a specialized data structure for collecting and aggregating infrastructure-related data points (e.g., URLs, hostnames, IPs, dependencies) during JavaScript AST analysis. It groups locations by value and file, with optional metadata support. Post-analysis, the collected data can be exploited externally (e.g., for network monitoring, security audits, or infrastructure mapping) to derive insights beyond JS-X-Ray's built-in warnings.
44

55
- **type**: Type
66
- **add(value, infos)**: Adds an entry to the set. Groups by value and file.
7+
- **values()**: Returns an iterable of all values of the CollectableSet.
78

89
CollectableSet is only an interface but Js-X-Ray provides a default implementation named DefaultCollectableSet.
910
The default implementation has an additional method to read what it has collected.
@@ -35,7 +36,7 @@ for (const { value, locations } of hostnameSet) {
3536
## API
3637

3738
```ts
38-
export type Type = "url" | "hostname" | "ip" | "email" | (string & {});
39+
export type Type = "url" | "hostname" | "ip" | "email" | "dependency" | (string & {});
3940

4041
export type Location<T = Record<string, unknown>> = {
4142
file: string | null;
@@ -52,6 +53,7 @@ export type CollectableInfos<T = Record<string, unknown>> = {
5253
export interface CollectableSet<T = Record<string, unknown>> {
5354
add(value: string, infos: CollectableInfos<T>): void;
5455
type: Type;
56+
values(): Iterable<string>;
5557
}
5658

5759
export class DefaultCollectableSet<T = Record<string, unknown>> implements CollectableSet<T> {

workspaces/js-x-ray/docs/EntryFilesAnalyser.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ declare class EntryFilesAnalyser {
4343
*/
4444
analyse(
4545
entryFiles: Iterable<string | URL>,
46-
options?: RuntimeFileOptions
46+
options?: RuntimeOptions
4747
): AsyncGenerator<ReportOnFile & { file: string }>;
4848
}
4949
```

workspaces/js-x-ray/src/AstAnalyser.ts

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,13 @@ import {
2929
type OptionalWarningName,
3030
type Warning
3131
} from "./warnings.ts";
32-
import type { CollectableSet } from "./CollectableSet.ts";
32+
import type { CollectableSet, Type } from "./CollectableSet.ts";
3333
import { CollectableSetRegistry } from "./CollectableSetRegistry.ts";
3434

35-
export interface Dependency {
35+
export type Dependency = {
3636
unsafe: boolean;
3737
inTry: boolean;
38-
location?: null | ESTree.SourceLocation;
39-
}
38+
};
4039

4140
export interface RuntimeOptions {
4241
/**
@@ -58,14 +57,10 @@ export interface RuntimeOptions {
5857
*/
5958
customParser?: SourceParser;
6059
metadata?: Record<string, unknown>;
61-
}
62-
63-
export interface RuntimeFileOptions extends Omit<RuntimeOptions, "isMinified"> {
6460
packageName?: string;
6561
}
6662

6763
export interface Report {
68-
dependencies: Map<string, Dependency>;
6964
warnings: Warning[];
7065
flags: Set<SourceFlags>;
7166
idsLengthAvg: number;
@@ -75,7 +70,6 @@ export interface Report {
7570
export type ReportOnFile = {
7671
ok: true;
7772
warnings: Warning[];
78-
dependencies: Map<string, Dependency>;
7973
flags: Set<SourceFlags>;
8074
} | {
8175
ok: false;
@@ -126,6 +120,7 @@ export class AstAnalyser {
126120
probes: Probe[];
127121
#collectables: CollectableSet[];
128122
#sensitivity: Sensitivity;
123+
#collectableSetRegistry: CollectableSetRegistry;
129124

130125
constructor(options: AstAnalyserOptions = {}) {
131126
const {
@@ -171,6 +166,7 @@ export class AstAnalyser {
171166
options: RuntimeOptions = {}
172167
): Report {
173168
const {
169+
packageName,
174170
location,
175171
isMinified = false,
176172
removeHTMLComments = false,
@@ -186,15 +182,22 @@ export class AstAnalyser {
186182
void 0
187183
);
188184

189-
const source = new SourceFile(location, metadata);
185+
const source = new SourceFile(location, {
186+
metadata,
187+
collectables: this.#collectables,
188+
packageName
189+
});
190+
191+
this.#collectableSetRegistry = source.collectablesSetRegistry;
192+
190193
source.sensitivity = this.#sensitivity;
191194
if (trojan.verify(str)) {
192195
source.warnings.push(
193196
generateWarning("obfuscated-code", { value: "trojan-source" })
194197
);
195198
}
196199

197-
const probeRunner = new ProbeRunner(source, new CollectableSetRegistry(this.#collectables), this.probes);
200+
const probeRunner = new ProbeRunner(source, this.probes);
198201
if (initialize) {
199202
if (typeof initialize !== "function") {
200203
throw new TypeError("options.initialize must be a function");
@@ -221,7 +224,6 @@ export class AstAnalyser {
221224

222225
return {
223226
...source.getResult(isMinified),
224-
dependencies: source.dependencies,
225227
flags: source.flags
226228
};
227229
}
@@ -250,11 +252,11 @@ export class AstAnalyser {
250252

251253
async analyseFile(
252254
pathToFile: string | URL,
253-
options: RuntimeFileOptions = {}
255+
options: RuntimeOptions = {}
254256
): Promise<ReportOnFile> {
255257
try {
256258
const {
257-
packageName = null,
259+
packageName,
258260
removeHTMLComments = false,
259261
initialize,
260262
finalize,
@@ -273,21 +275,17 @@ export class AstAnalyser {
273275
initialize,
274276
finalize,
275277
customParser,
276-
metadata
278+
metadata,
279+
packageName
277280
});
278281

279-
if (packageName !== null) {
280-
data.dependencies.delete(packageName);
281-
}
282-
283282
// Add is-minified flag if the file is minified and not a one-line require
284283
if (!data.flags.has("oneline-require") && isMin) {
285284
data.flags.add("is-minified");
286285
}
287286

288287
return {
289288
ok: true,
290-
dependencies: data.dependencies,
291289
warnings: data.warnings,
292290
flags: data.flags
293291
};
@@ -306,11 +304,11 @@ export class AstAnalyser {
306304

307305
analyseFileSync(
308306
pathToFile: string | URL,
309-
options: RuntimeFileOptions = {}
307+
options: RuntimeOptions = {}
310308
): ReportOnFile {
311309
try {
312310
const {
313-
packageName = null,
311+
packageName,
314312
removeHTMLComments = false,
315313
initialize,
316314
finalize,
@@ -329,21 +327,17 @@ export class AstAnalyser {
329327
initialize,
330328
finalize,
331329
customParser,
332-
metadata
330+
metadata,
331+
packageName
333332
});
334333

335-
if (packageName !== null) {
336-
data.dependencies.delete(packageName);
337-
}
338-
339334
// Add is-minified flag if the file is minified and not a one-line require
340335
if (!data.flags.has("oneline-require") && isMin) {
341336
data.flags.add("is-minified");
342337
}
343338

344339
return {
345340
ok: true,
346-
dependencies: data.dependencies,
347341
warnings: data.warnings,
348342
flags: data.flags
349343
};
@@ -384,4 +378,8 @@ export class AstAnalyser {
384378
#removeHTMLComment(str: string): string {
385379
return str.replaceAll(/<!--[\s\S]*?(?:-->)/g, "");
386380
}
381+
382+
getCollectableSet(type: Type) {
383+
return this.#collectableSetRegistry.get(type);
384+
}
387385
}

workspaces/js-x-ray/src/CollectableSet.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Import Internal Dependencies
22
import { type SourceArrayLocation } from "./utils/toArrayLocation.ts";
33

4-
export type Type = "url" | "hostname" | "ip" | "email" | (string & {});
4+
export type Type = "url" | "hostname" | "ip" | "email" | "dependency" | (string & {});
55

66
export type Location<T = Record<string, unknown>> = {
77
file: string | null;
@@ -18,6 +18,7 @@ export type CollectableInfos<T = Record<string, unknown>> = {
1818
export interface CollectableSet<T = Record<string, unknown>> {
1919
add(value: string, infos: CollectableInfos<T>): void;
2020
type: Type;
21+
values(): Iterable<string>;
2122
}
2223

2324
export class DefaultCollectableSet<T = Record<string, unknown>> implements CollectableSet<T> {
@@ -45,6 +46,10 @@ export class DefaultCollectableSet<T = Record<string, unknown>> implements Colle
4546
files?.set(file, [{ location, metadata }]);
4647
}
4748

49+
values(): Iterable<string> {
50+
return this.#entries.keys();
51+
}
52+
4853
* [Symbol.iterator]() {
4954
for (const [value, files] of this.#entries) {
5055
const locations: Location<T>[] = [];

workspaces/js-x-ray/src/CollectableSetRegistry.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@ export class CollectableSetRegistry {
2323
has(type: Type): boolean {
2424
return this.#collectableSets.has(type);
2525
}
26+
27+
get(type: Type): CollectableSet | undefined {
28+
return this.#collectableSets.get(type);
29+
}
2630
}

workspaces/js-x-ray/src/EntryFilesAnalyser.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import {
1515
AstAnalyser,
1616
type ReportOnFile,
17-
type RuntimeFileOptions
17+
type RuntimeOptions
1818
} from "./AstAnalyser.ts";
1919
import { TsSourceParser } from "./parsers/TsSourceParser.ts";
2020
import {
@@ -71,7 +71,7 @@ export class EntryFilesAnalyser {
7171

7272
async* analyse(
7373
entryFiles: Iterable<string | URL>,
74-
options: RuntimeFileOptions = {}
74+
options: RuntimeOptions = {}
7575
): AsyncGenerator<ReportOnFile & { file: string; }> {
7676
this.dependencies = new DiGraph();
7777

@@ -132,7 +132,7 @@ export class EntryFilesAnalyser {
132132
async* #analyseFile(
133133
file: string,
134134
relativeFile: string,
135-
options: RuntimeFileOptions
135+
options: RuntimeOptions
136136
) {
137137
this.dependencies.addVertex({
138138
id: relativeFile,
@@ -149,11 +149,13 @@ export class EntryFilesAnalyser {
149149
);
150150
yield { file: relativeFile, ...report };
151151

152-
if (!report.ok || typeof report.dependencies === "undefined") {
152+
const dependencySet = this.astAnalyzer.getCollectableSet("dependency");
153+
154+
if (!report.ok || typeof dependencySet === "undefined") {
153155
return;
154156
}
155157

156-
for (const [name] of report.dependencies) {
158+
for (const name of dependencySet.values()) {
157159
const depFile = await this.#getInternalDepPath(
158160
path.join(path.dirname(file), name)
159161
);

workspaces/js-x-ray/src/ProbeRunner.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import isPrototypePollution from "./probes/isPrototypePollution.ts";
2929
import type { TracedIdentifierReport } from "./VariableTracer.ts";
3030
import type { SourceFile } from "./SourceFile.ts";
3131
import type { OptionalWarningName } from "./warnings.ts";
32-
import type { CollectableSetRegistry } from "./CollectableSetRegistry.ts";
3332
import {
3433
getCallExpressionIdentifier
3534
} from "./estree/index.ts";
@@ -47,7 +46,6 @@ export type NamedMainHandlers<T extends ProbeContextDef = ProbeContextDef> = {
4746

4847
export type ProbeContext<T extends ProbeContextDef = ProbeContextDef> = {
4948
sourceFile: SourceFile;
50-
collectableSetRegistry: CollectableSetRegistry;
5149
context?: T;
5250
setEntryPoint: (handlerName: string) => void;
5351
};
@@ -75,7 +73,6 @@ export interface Probe<T extends ProbeContextDef = ProbeContextDef> {
7573
export class ProbeRunner {
7674
probes: Probe[];
7775
sourceFile: SourceFile;
78-
#collectableSetRegistry: CollectableSetRegistry;
7976
#selectedEntryPoints: Map<Probe, string> = new Map();
8077

8178
static Signals = Object.freeze({
@@ -116,11 +113,9 @@ export class ProbeRunner {
116113

117114
constructor(
118115
sourceFile: SourceFile,
119-
collectableSetRegistry: CollectableSetRegistry,
120116
probes: Probe[] = ProbeRunner.Defaults
121117
) {
122118
this.sourceFile = sourceFile;
123-
this.#collectableSetRegistry = collectableSetRegistry;
124119

125120
for (const probe of probes) {
126121
assert(
@@ -173,7 +168,6 @@ export class ProbeRunner {
173168

174169
return {
175170
sourceFile: this.sourceFile,
176-
collectableSetRegistry: this.#collectableSetRegistry,
177171
context: probe.context,
178172
setEntryPoint
179173
};

0 commit comments

Comments
 (0)