jsguardian 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,331 @@
1
+ "use strict";
2
+ // ============================================================================
3
+ // Layer 3e — Callgraph Poison (AI context-window flood)
4
+ //
5
+ // CONCEPT:
6
+ // Existing AI confusion layers (3/3b/3c) use instruction *text* — directive
7
+ // strings, legal notices, honeypot comments. A model that ignores policy
8
+ // strings (many do) bypasses them trivially.
9
+ //
10
+ // This layer attacks a different surface: **call-graph tracing cost**.
11
+ // AI models spend a fixed token budget tracing call graphs to understand
12
+ // what a function does. We inject N seeded-random functions that form
13
+ // mutual-recursion clusters — directed cycles with fake base-case guards.
14
+ //
15
+ // The model must trace each function to determine whether the recursion
16
+ // terminates (it never does, because the base-case constant is never
17
+ // reachable from outside), but that conclusion requires evaluating every
18
+ // call edge in the cluster. With 120–200 functions across 20–35 clusters,
19
+ // and 10% cross-cluster edges creating a weakly-connected tangle, the
20
+ // effective tracing cost is O(N × cluster_size) — exhausting context
21
+ // budgets before the model reaches real protected functions.
22
+ //
23
+ // PROPERTIES:
24
+ // - Zero instruction text: no "STOP", "HONEYPOT", or policy strings.
25
+ // Purely numeric arithmetic (XOR/AND/OR/shift/add) — looks like real
26
+ // obfuscated hash/cipher logic.
27
+ // - Seeded-random structure: every build produces different function names,
28
+ // parameter counts, body arithmetic constants, and cluster topology.
29
+ // - CNE-transparent: the `__cpn_*` names are renamed to `_0x...` by
30
+ // cneObfuscate → applyFullRename. MBA and string-cipher also run over
31
+ // the numeric constants. In the final output the functions are
32
+ // indistinguishable from real VM/cipher helpers.
33
+ // - Dead code: never called from user code, no module.exports assignment.
34
+ // `require(obfFile)` still exports exactly the same functions as before.
35
+ // - Scatter-injected: functions are `splice()`d at random positions in the
36
+ // AST body, not clumped at the end. The model cannot find real user
37
+ // functions by skipping a block at the top or bottom.
38
+ //
39
+ // TOPOLOGIES (4, randomly selected per cluster):
40
+ // linear: A→B→C→A (size 3–5, simple cycle)
41
+ // hub-spoke: hub→[A,B,C]; each spoke→hub (size 3–6)
42
+ // diamond: A→B,C; B→D; C→D; D→A (size 4, branching)
43
+ // pseudo: linear + every fn has `if(arg===<impossible_const>)return arg`
44
+ //
45
+ // CROSS-CLUSTER EDGES:
46
+ // 10% of functions additionally call one function from a different cluster,
47
+ // merging the clusters into a weakly-connected tangle.
48
+ // ============================================================================
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.applyCallgraphPoison = applyCallgraphPoison;
51
+ // ---------------------------------------------------------------------------
52
+ // Name generation — per-build seeded, identical format to CNE output names.
53
+ // Using _0x prefix directly means:
54
+ // (a) CNE applyFullRename skips them (matches /^_0x[0-9a-f]{4,}/) — correct,
55
+ // they’re already in the right format.
56
+ // (b) No __cpn_ fingerprint remains in the output at all.
57
+ // (c) In the final output they are indistinguishable from real VM/cipher helpers.
58
+ // Format mirrors makeNameGen in cne.ts: _0x + 4 hex + 2 hex = _0xXXXXXX.
59
+ // ---------------------------------------------------------------------------
60
+ function poisonName(rng, idx) {
61
+ const h = (rng.int32() ^ Math.imul(idx + 1, 0x9e3779b9)) >>> 0;
62
+ const part1 = (h & 0xffff).toString(16).padStart(4, "0");
63
+ const part2 = ((h >>> 16) & 0xff).toString(16).padStart(2, "0");
64
+ return `_0x${part1}${part2}`;
65
+ }
66
+ // ---------------------------------------------------------------------------
67
+ // Arithmetic expression generators — all produce valid JS numeric expressions.
68
+ // Use seeded-random constants so every build is structurally different.
69
+ // ---------------------------------------------------------------------------
70
+ function randConst(rng) {
71
+ const c = (rng.int32() >>> 0) & 0xffff;
72
+ return `0x${c.toString(16)}`;
73
+ }
74
+ function randOp(rng, a, b) {
75
+ const ops = [
76
+ `(${a} ^ ${randConst(rng)})`,
77
+ `(${a} & ${randConst(rng)})`,
78
+ `(${a} | ${randConst(rng)})`,
79
+ `((${a} + ${randConst(rng)}) | 0)`,
80
+ `(${a} >>> ${1 + (rng.int32() & 0xf)})`,
81
+ `(~${a} & ${randConst(rng)})`,
82
+ `((${a} ^ ${b}) & ${randConst(rng)})`,
83
+ `((${a} + ${b}) >>> ${1 + (rng.int32() & 0x7)})`,
84
+ ];
85
+ return ops[Math.floor(rng.next() * ops.length)];
86
+ }
87
+ // ---------------------------------------------------------------------------
88
+ // Function body source builder.
89
+ // paramNames: the function's own parameters
90
+ // callTargets: 1–2 peer function names to tail-call
91
+ // fakeBaseConst: large random hex constant for the impossible base-case guard
92
+ // includePseudo: if true, add the impossible base-case if statement
93
+ // ---------------------------------------------------------------------------
94
+ function buildFunctionBody(fnName, paramNames, callTargets, fakeBaseConst, includePseudo, rng) {
95
+ const p0 = paramNames[0] ?? "a";
96
+ const p1 = paramNames[1] ?? "b";
97
+ const lines = [];
98
+ lines.push(`function ${fnName}(${paramNames.join(", ")}) {`);
99
+ // Fake base case — the constant is random and large, never passed externally.
100
+ if (includePseudo || rng.next() < 0.55) {
101
+ // Return one of the params (random choice) to look plausible
102
+ const retParam = paramNames[Math.floor(rng.next() * paramNames.length)] ?? "a";
103
+ lines.push(` if (${p0} === ${fakeBaseConst}) return ${retParam};`);
104
+ }
105
+ // 0–2 intermediate variable assignments that look like cipher rounds.
106
+ const numVars = Math.floor(rng.next() * 3); // 0..2
107
+ const varNames = [];
108
+ for (let i = 0; i < numVars; i++) {
109
+ const v = `_${(rng.int32() >>> 0 & 0xfff).toString(16)}`;
110
+ varNames.push(v);
111
+ // operands: mix params and previously assigned vars
112
+ const sources = [...paramNames, ...varNames.slice(0, -1)];
113
+ const src0 = sources[Math.floor(rng.next() * sources.length)];
114
+ const src1 = sources[Math.floor(rng.next() * sources.length)];
115
+ lines.push(` var ${v} = ${randOp(rng, src0, src1)};`);
116
+ }
117
+ // Build call arguments — use vars + params, mixed with small constants
118
+ const allSources = [...paramNames, ...varNames];
119
+ const buildArg = () => {
120
+ const base = allSources[Math.floor(rng.next() * allSources.length)] ?? p0;
121
+ if (rng.next() < 0.35)
122
+ return `(${base} & 0xff)`;
123
+ if (rng.next() < 0.35)
124
+ return `(${base} ^ ${randConst(rng)})`;
125
+ return base;
126
+ };
127
+ // Tail call — return the result of calling a peer
128
+ const target0 = callTargets[0];
129
+ if (callTargets.length >= 2 && rng.next() < 0.35) {
130
+ // Diamond/branching: two peers, pick one based on a runtime expression
131
+ const target1 = callTargets[1];
132
+ const pred = `(${p0} & 1)`;
133
+ const args0 = paramNames.map(() => buildArg()).join(", ");
134
+ const args1 = paramNames.map(() => buildArg()).join(", ");
135
+ lines.push(` return (${pred}) ? ${target0}(${args0}) : ${target1}(${args1});`);
136
+ }
137
+ else {
138
+ const args = paramNames.map(() => buildArg()).join(", ");
139
+ lines.push(` return ${target0}(${args});`);
140
+ }
141
+ lines.push(`}`);
142
+ return lines.join("\n");
143
+ }
144
+ // ---------------------------------------------------------------------------
145
+ // Topology builders — return an array of {name, src} pairs for one cluster
146
+ // ---------------------------------------------------------------------------
147
+ function buildLinearCluster(names, rng, pseudo) {
148
+ // A→B→C→...→A
149
+ const result = [];
150
+ for (let i = 0; i < names.length; i++) {
151
+ const fn = names[i];
152
+ const next = names[(i + 1) % names.length];
153
+ const pcount = 1 + Math.floor(rng.next() * 3); // 1..3
154
+ const params = Array.from({ length: pcount }, (_, k) => ["a", "b", "c"][k] ?? `p${k}`);
155
+ const fakeBase = `0x${((rng.int32() >>> 0) | 0x80000000).toString(16)}`;
156
+ const src = buildFunctionBody(fn, params, [next], fakeBase, pseudo, rng);
157
+ result.push({ name: fn, src });
158
+ }
159
+ return result;
160
+ }
161
+ function buildHubSpokeCluster(names, rng, pseudo) {
162
+ // names[0] = hub, names[1..] = spokes
163
+ const hub = names[0];
164
+ const spokes = names.slice(1);
165
+ const result = [];
166
+ // Hub calls a random spoke each time (runtime-selected by bitmask)
167
+ const pcount = 1 + Math.floor(rng.next() * 3);
168
+ const hubParams = Array.from({ length: pcount }, (_, k) => ["a", "b", "c"][k] ?? `p${k}`);
169
+ const fakeBase = `0x${((rng.int32() >>> 0) | 0x80000000).toString(16)}`;
170
+ // Hub body: calls the spoke chosen by (a & (spokes.length-1))
171
+ // We generate it manually since it needs dynamic dispatch
172
+ const hubLines = [
173
+ `function ${hub}(${hubParams.join(", ")}) {`,
174
+ ` if (${hubParams[0]} === ${fakeBase}) return ${hubParams[0]};`,
175
+ ` switch (${hubParams[0]} & ${spokes.length - 1}) {`,
176
+ ];
177
+ spokes.forEach((spoke, si) => {
178
+ const args = hubParams.map(p => `(${p} ^ 0x${(rng.int32() & 0xff).toString(16)})`).join(", ");
179
+ hubLines.push(` case ${si}: return ${spoke}(${args});`);
180
+ });
181
+ hubLines.push(` default: return ${spokes[0]}(${hubParams.join(", ")});`);
182
+ hubLines.push(` }`);
183
+ hubLines.push(`}`);
184
+ result.push({ name: hub, src: hubLines.join("\n") });
185
+ // Each spoke calls hub back
186
+ for (const spoke of spokes) {
187
+ const sp = 1 + Math.floor(rng.next() * 3);
188
+ const spokeParams = Array.from({ length: sp }, (_, k) => ["a", "b", "c"][k] ?? `p${k}`);
189
+ const fb = `0x${((rng.int32() >>> 0) | 0x80000000).toString(16)}`;
190
+ const src = buildFunctionBody(spoke, spokeParams, [hub], fb, pseudo, rng);
191
+ result.push({ name: spoke, src });
192
+ }
193
+ return result;
194
+ }
195
+ function buildDiamondCluster(names, rng, pseudo) {
196
+ // A→B,C; B→D; C→D; D→A (padded for larger clusters)
197
+ // For size > 4 we use: A→B,C; B→D; C→E; D→A; E→A
198
+ const result = [];
199
+ for (let i = 0; i < names.length; i++) {
200
+ const fn = names[i];
201
+ const pcount = 1 + Math.floor(rng.next() * 3);
202
+ const params = Array.from({ length: pcount }, (_, k) => ["a", "b", "c"][k] ?? `p${k}`);
203
+ const fakeBase = `0x${((rng.int32() >>> 0) | 0x80000000).toString(16)}`;
204
+ // Each fn calls next and, 50% of the time, the one two steps ahead
205
+ const next0 = names[(i + 1) % names.length];
206
+ const next1 = names[(i + 2) % names.length];
207
+ const targets = rng.next() < 0.5 ? [next0, next1] : [next0];
208
+ const src = buildFunctionBody(fn, params, targets, fakeBase, pseudo, rng);
209
+ result.push({ name: fn, src });
210
+ }
211
+ return result;
212
+ }
213
+ function buildCluster(names, rng) {
214
+ if (names.length < 2)
215
+ return []; // need at least 2
216
+ const pseudo = rng.next() < 0.5; // half clusters have explicit pseudo-base-case
217
+ const topo = Math.floor(rng.next() * 4);
218
+ switch (topo) {
219
+ case 0: return buildLinearCluster(names, rng, pseudo);
220
+ case 1: return buildHubSpokeCluster(names, rng, pseudo);
221
+ case 2: return buildDiamondCluster(names, rng, pseudo);
222
+ default: return buildLinearCluster(names, rng, true); // pseudo forced
223
+ }
224
+ }
225
+ // ---------------------------------------------------------------------------
226
+ // Cross-cluster edge injection — rewrites the return statement of one function
227
+ // to sometimes call into a sibling cluster. We do this by appending an extra
228
+ // "else" call that is reached when the main cluster detects a specific bit.
229
+ // Actually simpler: we add a helper function whose source calls across clusters.
230
+ // We don't want to rewrite already-generated source, so we generate a small
231
+ // "bridge" function that calls one fn from each of two clusters.
232
+ // ---------------------------------------------------------------------------
233
+ function buildBridgeFn(bridgeName, srcFn, dstFn, rng) {
234
+ const pcount = 1 + Math.floor(rng.next() * 2);
235
+ const params = Array.from({ length: pcount }, (_, k) => ["a", "b"][k] ?? `p${k}`);
236
+ const fakeBase = `0x${((rng.int32() >>> 0) | 0x80000000).toString(16)}`;
237
+ const lines = [
238
+ `function ${bridgeName}(${params.join(", ")}) {`,
239
+ ` if (${params[0]} === ${fakeBase}) return ${params[0]};`,
240
+ ` return (${params[0]} & 1) ? ${srcFn}(${params.join(", ")}) : ${dstFn}(${params.join(", ")});`,
241
+ `}`,
242
+ ];
243
+ return lines.join("\n");
244
+ }
245
+ // ---------------------------------------------------------------------------
246
+ // Main export
247
+ // ---------------------------------------------------------------------------
248
+ /**
249
+ * Inject `count` seeded-random mutual-recursion functions into the AST body,
250
+ * scattered at random positions. Functions form directed cycles (clusters)
251
+ * with 10% cross-cluster bridge functions for a tangle effect.
252
+ *
253
+ * The injected functions are dead code — never called from user code.
254
+ * `module.exports` is unchanged. All existing roundtrip tests still pass.
255
+ *
256
+ * After CNE `applyFullRename`, the `__cpn_*` names become `_0x...` and are
257
+ * indistinguishable from real VM/cipher helpers. MBA also runs over the
258
+ * numeric constants.
259
+ */
260
+ function applyCallgraphPoison(ast, _t, parse, rng, count) {
261
+ if (count <= 0)
262
+ return;
263
+ // ── 1. Generate all names ─────────────────────────────────────────────────
264
+ // Reserve ~10% of names for bridge functions (cross-cluster edges).
265
+ const bridgeCount = Math.max(1, Math.floor(count * 0.10));
266
+ const clusterCount = count - bridgeCount;
267
+ const clusterNames = [];
268
+ for (let i = 0; i < clusterCount; i++) {
269
+ clusterNames.push(poisonName(rng, i));
270
+ }
271
+ const bridgeNames = [];
272
+ for (let i = 0; i < bridgeCount; i++) {
273
+ bridgeNames.push(poisonName(rng, clusterCount + i));
274
+ }
275
+ // ── 2. Assign cluster members ──────────────────────────────────────────────
276
+ const clusters = [];
277
+ let ci = 0;
278
+ while (ci < clusterNames.length) {
279
+ const size = 3 + Math.floor(rng.next() * 5); // 3..7
280
+ clusters.push(clusterNames.slice(ci, ci + Math.min(size, clusterNames.length - ci)));
281
+ ci += size;
282
+ }
283
+ // ── 3. Build function sources ──────────────────────────────────────────────
284
+ const allEntries = [];
285
+ for (const cluster of clusters) {
286
+ if (cluster.length < 2)
287
+ continue; // skip singletons
288
+ const entries = buildCluster(cluster, rng);
289
+ allEntries.push(...entries);
290
+ }
291
+ // ── 4. Cross-cluster bridges ───────────────────────────────────────────────
292
+ if (clusters.length >= 2) {
293
+ for (let bi = 0; bi < bridgeNames.length; bi++) {
294
+ const ci0 = Math.floor(rng.next() * clusters.length);
295
+ let ci1 = Math.floor(rng.next() * clusters.length);
296
+ if (ci1 === ci0)
297
+ ci1 = (ci0 + 1) % clusters.length;
298
+ const srcFn = clusters[ci0][Math.floor(rng.next() * clusters[ci0].length)];
299
+ const dstFn = clusters[ci1][Math.floor(rng.next() * clusters[ci1].length)];
300
+ if (!srcFn || !dstFn)
301
+ continue;
302
+ const src = buildBridgeFn(bridgeNames[bi], srcFn, dstFn, rng);
303
+ allEntries.push({ name: bridgeNames[bi], src });
304
+ }
305
+ }
306
+ // ── 5. Parse each function and inject into AST at a random position ────────
307
+ const body = ast.program.body;
308
+ for (const entry of allEntries) {
309
+ let fnNode;
310
+ try {
311
+ const mini = parse(entry.src, {
312
+ sourceType: "module",
313
+ allowReturnOutsideFunction: true,
314
+ errorRecovery: false,
315
+ });
316
+ fnNode = mini.program.body[0];
317
+ if (!fnNode)
318
+ continue;
319
+ }
320
+ catch {
321
+ // If source generation produced invalid JS (shouldn't happen), skip.
322
+ continue;
323
+ }
324
+ // Mark the FunctionDeclaration node __obf to prevent VM virtualization
325
+ // but leave inner body nodes unmarked so MBA/string-cipher/CNE can run.
326
+ fnNode.__obf = true;
327
+ // Scatter at a random position in the current body.
328
+ const pos = Math.floor(rng.next() * (body.length + 1));
329
+ body.splice(pos, 0, fnNode);
330
+ }
331
+ }