@7nsane/zift 3.0.0 → 4.1.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.
- package/README.md +9 -8
- package/package.json +1 -1
- package/src/collector.js +73 -10
- package/src/rules/definitions.js +28 -1
- package/src/scanner.js +50 -9
- package/src/shield.js +48 -7
package/README.md
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
# 🛡️ Zift (
|
|
1
|
+
# 🛡️ Zift (v4.1.0)
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@7nsane/zift)
|
|
4
4
|
[](https://www.npmjs.com/package/@7nsane/zift)
|
|
5
5
|
[](https://github.com/7nsane/zift)
|
|
6
6
|
|
|
7
|
-
**The Intelligent Ecosystem Security Engine for JavaScript.**
|
|
7
|
+
**The Symbolically-Intelligent Ecosystem Security Engine for JavaScript.**
|
|
8
8
|
|
|
9
|
-
Zift
|
|
9
|
+
Zift v5.0 is the "Intelligence" release, introducing **Symbolic Taint Analysis**. It can track sensitive data through complex code transformations, destructuring, and nested object structures across module boundaries.
|
|
10
10
|
|
|
11
|
-
## 🚀
|
|
11
|
+
## 🚀 Key Advancements (v5.0.0)
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
13
|
+
- **🧠 Symbolic Taint Analysis**: Tracks data through destructuring (`const { key } = process.env`) and deep property access (`obj.a.b.c`).
|
|
14
|
+
- **🧬 Transformation Tracking**: Automatically follows taint through encoding methods like `Buffer.from(data).toString('base64')` or `hex`.
|
|
15
|
+
- **🌍 Recursive Cross-File Intelligence**: Follows sensitive data even when it's re-exported through multiple intermediate files and objects.
|
|
16
|
+
- **🛡️ Immutable Runtime Guards**: Structural protection for `http` and `child_process` sinks (v4.0 legacy).
|
|
17
|
+
- **🧩 Opaque Payload Detection**: Flags native binaries (`.node`) and high-entropy skipped strings.
|
|
17
18
|
|
|
18
19
|
## 📦 Quick Start
|
|
19
20
|
|
package/package.json
CHANGED
package/src/collector.js
CHANGED
|
@@ -27,7 +27,9 @@ class ASTCollector {
|
|
|
27
27
|
REMOTE_FETCH_SIGNAL: [],
|
|
28
28
|
PIPE_TO_SHELL_SIGNAL: [],
|
|
29
29
|
EXPORTS: [],
|
|
30
|
-
IMPORTS: []
|
|
30
|
+
IMPORTS: [],
|
|
31
|
+
OPAQUE_STRING_SKIP: [],
|
|
32
|
+
NON_DETERMINISTIC_SINK: []
|
|
31
33
|
};
|
|
32
34
|
const flows = [];
|
|
33
35
|
const sourceCode = code;
|
|
@@ -46,7 +48,20 @@ class ASTCollector {
|
|
|
46
48
|
|
|
47
49
|
walk.ancestor(ast, {
|
|
48
50
|
Literal: (node) => {
|
|
49
|
-
if (typeof node.value === 'string' && node.value.length > 20
|
|
51
|
+
if (typeof node.value === 'string' && node.value.length > 20) {
|
|
52
|
+
if (node.value.length > this.maxStringLengthForEntropy) {
|
|
53
|
+
// High Entropy Skip Warning
|
|
54
|
+
const sample = node.value.substring(0, 100);
|
|
55
|
+
const sampleEntropy = calculateEntropy(sample);
|
|
56
|
+
if (sampleEntropy > this.entropyThreshold) {
|
|
57
|
+
facts.OPAQUE_STRING_SKIP.push({
|
|
58
|
+
file: filePath,
|
|
59
|
+
line: node.loc.start.line,
|
|
60
|
+
reason: `Large string skipped (>2KB) but sample has high entropy (${sampleEntropy.toFixed(2)})`
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
50
65
|
const entropy = calculateEntropy(node.value);
|
|
51
66
|
if (entropy > this.entropyThreshold) {
|
|
52
67
|
facts.OBFUSCATION.push({
|
|
@@ -138,7 +153,6 @@ class ASTCollector {
|
|
|
138
153
|
}
|
|
139
154
|
}
|
|
140
155
|
|
|
141
|
-
// De-obfuscation Trigger
|
|
142
156
|
const evaluated = this.tryEvaluate(node, sourceCode);
|
|
143
157
|
if (evaluated) {
|
|
144
158
|
if (this.getNetworkType(evaluated) || this.isShellSink(evaluated) || evaluated === 'eval' || evaluated === 'Function') {
|
|
@@ -226,7 +240,36 @@ class ASTCollector {
|
|
|
226
240
|
});
|
|
227
241
|
}
|
|
228
242
|
}
|
|
243
|
+
|
|
244
|
+
// v4.0 Hardening: Non-deterministic constructor
|
|
245
|
+
if (['Math.random', 'Date.now', 'Date()'].some(t => argCode.includes(t))) {
|
|
246
|
+
if (evaluated === 'eval' || evaluated === 'Function' || this.isShellSink(calleeCode)) {
|
|
247
|
+
facts.NON_DETERMINISTIC_SINK.push({
|
|
248
|
+
file: filePath,
|
|
249
|
+
line: node.loc.start.line,
|
|
250
|
+
callee: calleeCode,
|
|
251
|
+
reason: `Sink uses non-deterministic argument (${argCode})`
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
229
255
|
});
|
|
256
|
+
|
|
257
|
+
// v5.0 Symbolic Transformers: Buffer/Base64/Hex
|
|
258
|
+
if (calleeCode.includes('Buffer.from') || calleeCode.includes('.toString')) {
|
|
259
|
+
const parent = ancestors[ancestors.length - 2];
|
|
260
|
+
if (parent && parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
|
|
261
|
+
const arg = node.arguments[0] ? sourceCode.substring(node.arguments[0].start, node.arguments[0].end) : null;
|
|
262
|
+
if (arg) {
|
|
263
|
+
flows.push({
|
|
264
|
+
fromVar: arg,
|
|
265
|
+
toVar: parent.id.name,
|
|
266
|
+
file: filePath,
|
|
267
|
+
line: node.loc.start.line,
|
|
268
|
+
transformation: calleeCode.includes('base64') ? 'base64' : (calleeCode.includes('hex') ? 'hex' : 'buffer')
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
230
273
|
},
|
|
231
274
|
MemberExpression: (node) => {
|
|
232
275
|
const objectCode = sourceCode.substring(node.object.start, node.object.end);
|
|
@@ -248,14 +291,9 @@ class ASTCollector {
|
|
|
248
291
|
}
|
|
249
292
|
},
|
|
250
293
|
VariableDeclarator: (node) => {
|
|
251
|
-
if (node.init
|
|
294
|
+
if (node.init) {
|
|
252
295
|
const from = sourceCode.substring(node.init.start, node.init.end);
|
|
253
|
-
flows.
|
|
254
|
-
fromVar: from,
|
|
255
|
-
toVar: node.id.name,
|
|
256
|
-
file: filePath,
|
|
257
|
-
line: node.loc.start.line
|
|
258
|
-
});
|
|
296
|
+
this.handlePattern(node.id, from, flows, filePath, node.loc.start.line);
|
|
259
297
|
}
|
|
260
298
|
},
|
|
261
299
|
AssignmentExpression: (node) => {
|
|
@@ -279,6 +317,9 @@ class ASTCollector {
|
|
|
279
317
|
file: filePath,
|
|
280
318
|
line: node.loc.start.line
|
|
281
319
|
});
|
|
320
|
+
} else if (node.left.type === 'ObjectPattern' || node.left.type === 'ArrayPattern') {
|
|
321
|
+
const from = sourceCode.substring(node.right.start, node.right.end);
|
|
322
|
+
this.handlePattern(node.left, from, flows, filePath, node.loc.start.line);
|
|
282
323
|
}
|
|
283
324
|
},
|
|
284
325
|
ObjectExpression: (node, state, ancestors) => {
|
|
@@ -395,6 +436,28 @@ class ASTCollector {
|
|
|
395
436
|
return null;
|
|
396
437
|
}
|
|
397
438
|
}
|
|
439
|
+
|
|
440
|
+
handlePattern(pattern, initCode, flows, filePath, line) {
|
|
441
|
+
if (pattern.type === 'Identifier') {
|
|
442
|
+
flows.push({ fromVar: initCode, toVar: pattern.name, file: filePath, line });
|
|
443
|
+
} else if (pattern.type === 'ObjectPattern') {
|
|
444
|
+
pattern.properties.forEach(prop => {
|
|
445
|
+
if (prop.type === 'Property') {
|
|
446
|
+
const key = prop.key.type === 'Identifier' ? prop.key.name :
|
|
447
|
+
(prop.key.type === 'Literal' ? prop.key.value : null);
|
|
448
|
+
if (key) {
|
|
449
|
+
this.handlePattern(prop.value, `${initCode}.${key}`, flows, filePath, line);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
} else if (pattern.type === 'ArrayPattern') {
|
|
454
|
+
pattern.elements.forEach((el, index) => {
|
|
455
|
+
if (el) {
|
|
456
|
+
this.handlePattern(el, `${initCode}[${index}]`, flows, filePath, line);
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
398
461
|
}
|
|
399
462
|
|
|
400
463
|
module.exports = ASTCollector;
|
package/src/rules/definitions.js
CHANGED
|
@@ -110,13 +110,40 @@ const RULES = [
|
|
|
110
110
|
priority: 2,
|
|
111
111
|
baseScore: 60,
|
|
112
112
|
description: 'Detection of attempts to modify package.json scripts or npm configuration.'
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'ZFT-013',
|
|
116
|
+
alias: 'OPAQUE_BINARY_PAYLOAD',
|
|
117
|
+
name: 'Opaque Binary Payload',
|
|
118
|
+
requires: ['NATIVE_BINARY_DETECTED'],
|
|
119
|
+
priority: 2,
|
|
120
|
+
baseScore: 40,
|
|
121
|
+
description: 'Detection of compiled native binaries (.node) which are opaque to static analysis.'
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'ZFT-014',
|
|
125
|
+
alias: 'EVASIVE_SINK_CONSTRUCTION',
|
|
126
|
+
name: 'Evasive Sink Construction',
|
|
127
|
+
requires: ['NON_DETERMINISTIC_SINK'],
|
|
128
|
+
priority: 3,
|
|
129
|
+
baseScore: 50,
|
|
130
|
+
description: 'Detection of dangerous sinks using non-deterministic construction (Date.now, Math.random) to evade analysis.'
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
id: 'ZFT-015',
|
|
134
|
+
alias: 'HIGH_ENTROPY_OPAQUE_STRING',
|
|
135
|
+
name: 'High Entropy Opaque String',
|
|
136
|
+
requires: ['OPAQUE_STRING_SKIP'],
|
|
137
|
+
priority: 1,
|
|
138
|
+
baseScore: 25,
|
|
139
|
+
description: 'Detection of very large high-entropy strings that exceed scanning limits.'
|
|
113
140
|
}
|
|
114
141
|
];
|
|
115
142
|
|
|
116
143
|
const CATEGORIES = {
|
|
117
144
|
SOURCES: ['ENV_READ', 'FILE_READ_SENSITIVE', 'MASS_ENV_ACCESS'],
|
|
118
145
|
SINKS: ['NETWORK_SINK', 'DNS_SINK', 'RAW_SOCKET_SINK', 'DYNAMIC_EXECUTION', 'SHELL_EXECUTION', 'DYNAMIC_REQUIRE'],
|
|
119
|
-
SIGNALS: ['OBFUSCATION', 'ENCODER_USE', 'REMOTE_FETCH_SIGNAL', 'PIPE_TO_SHELL_SIGNAL'],
|
|
146
|
+
SIGNALS: ['OBFUSCATION', 'ENCODER_USE', 'REMOTE_FETCH_SIGNAL', 'PIPE_TO_SHELL_SIGNAL', 'NATIVE_BINARY_DETECTED', 'OPAQUE_STRING_SKIP', 'NON_DETERMINISTIC_SINK'],
|
|
120
147
|
PERSISTENCE: ['FILE_WRITE_STARTUP'],
|
|
121
148
|
CONTEXT: ['LIFECYCLE_CONTEXT']
|
|
122
149
|
};
|
package/src/scanner.js
CHANGED
|
@@ -43,7 +43,9 @@ class PackageScanner {
|
|
|
43
43
|
PIPE_TO_SHELL_SIGNAL: [],
|
|
44
44
|
LIFECYCLE_CONTEXT: [],
|
|
45
45
|
EXPORTS: [],
|
|
46
|
-
IMPORTS: []
|
|
46
|
+
IMPORTS: [],
|
|
47
|
+
NATIVE_BINARY_DETECTED: [],
|
|
48
|
+
OPAQUE_STRING_SKIP: []
|
|
47
49
|
},
|
|
48
50
|
flows: []
|
|
49
51
|
};
|
|
@@ -55,6 +57,13 @@ class PackageScanner {
|
|
|
55
57
|
for (let i = 0; i < files.length; i += concurrency) {
|
|
56
58
|
const chunk = files.slice(i, i + concurrency);
|
|
57
59
|
await Promise.all(chunk.map(async (file) => {
|
|
60
|
+
if (file.endsWith('.node')) {
|
|
61
|
+
allFacts.facts.NATIVE_BINARY_DETECTED.push({
|
|
62
|
+
file,
|
|
63
|
+
reason: 'Compiled native binary detected (Opaque Payload)'
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
58
67
|
const stats = fs.statSync(file);
|
|
59
68
|
if (stats.size > 512 * 1024) return;
|
|
60
69
|
|
|
@@ -108,13 +117,13 @@ class PackageScanner {
|
|
|
108
117
|
facts.EXPORTS.forEach(exp => {
|
|
109
118
|
if (!exportMap.has(exp.file)) exportMap.set(exp.file, new Map());
|
|
110
119
|
|
|
111
|
-
// Check if localName is tainted
|
|
112
|
-
const
|
|
113
|
-
const isNamedTainted = flows.some(f => f.file === exp.file && f.toVar === exp.name && f.fromVar.includes('process.env'));
|
|
120
|
+
// Check if localName is tainted (recursively)
|
|
121
|
+
const resolvedTaint = this.isVariableTainted(exp.local || exp.name, exp.file, flows);
|
|
114
122
|
|
|
115
123
|
exportMap.get(exp.file).set(exp.name, {
|
|
116
124
|
local: exp.local,
|
|
117
|
-
isTainted:
|
|
125
|
+
isTainted: !!resolvedTaint,
|
|
126
|
+
taintPath: resolvedTaint
|
|
118
127
|
});
|
|
119
128
|
});
|
|
120
129
|
|
|
@@ -123,7 +132,7 @@ class PackageScanner {
|
|
|
123
132
|
let resolvedPath;
|
|
124
133
|
if (imp.source.startsWith('.')) {
|
|
125
134
|
resolvedPath = path.resolve(path.dirname(imp.file), imp.source);
|
|
126
|
-
if (!resolvedPath.endsWith('.js')) resolvedPath += '.js';
|
|
135
|
+
if (!resolvedPath.endsWith('.js') && fs.existsSync(resolvedPath + '.js')) resolvedPath += '.js';
|
|
127
136
|
}
|
|
128
137
|
|
|
129
138
|
if (resolvedPath && exportMap.has(resolvedPath)) {
|
|
@@ -131,18 +140,50 @@ class PackageScanner {
|
|
|
131
140
|
const matchedExport = targetExports.get(imp.imported);
|
|
132
141
|
|
|
133
142
|
if (matchedExport && matchedExport.isTainted) {
|
|
134
|
-
// Mark as a virtual ENV_READ in the importing file
|
|
135
143
|
facts.ENV_READ.push({
|
|
136
144
|
file: imp.file,
|
|
137
145
|
line: imp.line,
|
|
138
|
-
variable: `[Cross-File] ${imp.local} (from ${imp.source})`,
|
|
146
|
+
variable: `[Symbolic Cross-File] ${imp.local} <- ${matchedExport.taintPath} (from ${imp.source})`,
|
|
139
147
|
isCrossFile: true
|
|
140
148
|
});
|
|
149
|
+
|
|
150
|
+
// Propagate further as a flow in this file
|
|
151
|
+
flows.push({
|
|
152
|
+
fromVar: matchedExport.taintPath,
|
|
153
|
+
toVar: imp.local,
|
|
154
|
+
file: imp.file,
|
|
155
|
+
line: imp.line,
|
|
156
|
+
type: 'cross-file-import'
|
|
157
|
+
});
|
|
141
158
|
}
|
|
142
159
|
}
|
|
143
160
|
});
|
|
144
161
|
}
|
|
145
162
|
|
|
163
|
+
isVariableTainted(varName, filePath, flows, visited = new Set()) {
|
|
164
|
+
if (!varName) return null;
|
|
165
|
+
const key = `${filePath}:${varName}`;
|
|
166
|
+
if (visited.has(key)) return null;
|
|
167
|
+
visited.add(key);
|
|
168
|
+
|
|
169
|
+
if (varName.includes('process.env')) return varName;
|
|
170
|
+
|
|
171
|
+
const incoming = flows.filter(f => f.file === filePath && f.toVar === varName);
|
|
172
|
+
for (const flow of incoming) {
|
|
173
|
+
const result = this.isVariableTainted(flow.fromVar, filePath, flows, visited);
|
|
174
|
+
if (result) return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check for property access patterns if base object is tainted
|
|
178
|
+
if (varName.includes('.')) {
|
|
179
|
+
const base = varName.split('.')[0];
|
|
180
|
+
const result = this.isVariableTainted(base, filePath, flows, visited);
|
|
181
|
+
if (result) return `${result}.${varName.split('.').slice(1).join('.')}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
146
187
|
async getFiles() {
|
|
147
188
|
// Load .ziftignore
|
|
148
189
|
const ziftIgnorePath = path.join(this.packageDir, '.ziftignore');
|
|
@@ -166,7 +207,7 @@ class PackageScanner {
|
|
|
166
207
|
const stat = fs.statSync(fullPath);
|
|
167
208
|
if (stat && stat.isDirectory()) {
|
|
168
209
|
results.push(...getJsFiles(fullPath));
|
|
169
|
-
} else if (file.endsWith('.js')) {
|
|
210
|
+
} else if (file.endsWith('.js') || file.endsWith('.node')) {
|
|
170
211
|
results.push(fullPath);
|
|
171
212
|
}
|
|
172
213
|
}
|
package/src/shield.js
CHANGED
|
@@ -14,31 +14,38 @@ function setupShield() {
|
|
|
14
14
|
console.warn(`[ZIFT-SHIELD] 🌐 Outbound Connection: ${address}:${port}`);
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
// 2. Wrap Child Process (for shell command execution)
|
|
17
|
+
// 2. Wrap Child Process (for shell command execution) - IMMUTABLE
|
|
18
18
|
const cp = require('node:child_process');
|
|
19
19
|
['exec', 'spawn', 'execSync', 'spawnSync'].forEach(method => {
|
|
20
20
|
const original = cp[method];
|
|
21
|
-
|
|
21
|
+
if (!original) return;
|
|
22
|
+
|
|
23
|
+
const wrapper = function (...args) {
|
|
22
24
|
const command = args[0];
|
|
23
|
-
const cmdStr = typeof command === 'string' ? command : (args[1] ? args[1].join(' ') :
|
|
25
|
+
const cmdStr = typeof command === 'string' ? command : (Array.isArray(args[1]) ? args[1].join(' ') : String(command));
|
|
24
26
|
console.warn(`[ZIFT-SHIELD] 🐚 Shell Execution: ${cmdStr}`);
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
if (cmdStr.includes('curl') || cmdStr.includes('wget') || cmdStr.includes('| sh')) {
|
|
28
|
+
if (cmdStr.includes('curl') || cmdStr.includes('wget') || cmdStr.includes('| sh') || cmdStr.includes('| bash')) {
|
|
28
29
|
console.error(`[ZIFT-SHIELD] ⚠️ CRITICAL: Potential Remote Dropper detected in shell execution!`);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
return original.apply(this, args);
|
|
32
33
|
};
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
Object.defineProperty(cp, method, { value: wrapper, writable: false, configurable: false });
|
|
37
|
+
} catch (e) {
|
|
38
|
+
cp[method] = wrapper; // Fallback
|
|
39
|
+
}
|
|
33
40
|
});
|
|
34
41
|
|
|
35
|
-
// 3. Monitor HTTP/HTTPS
|
|
42
|
+
// 3. Monitor HTTP/HTTPS - IMMUTABLE
|
|
36
43
|
const http = require('node:http');
|
|
37
44
|
const https = require('node:https');
|
|
38
45
|
[http, https].forEach(mod => {
|
|
39
46
|
['request', 'get'].forEach(method => {
|
|
40
47
|
const original = mod[method];
|
|
41
|
-
|
|
48
|
+
const wrapper = function (...args) {
|
|
42
49
|
let url = args[0];
|
|
43
50
|
if (typeof url === 'object' && url.href) url = url.href;
|
|
44
51
|
else if (typeof url === 'string') url = url;
|
|
@@ -47,8 +54,42 @@ function setupShield() {
|
|
|
47
54
|
console.warn(`[ZIFT-SHIELD] 📡 HTTP Request: ${url}`);
|
|
48
55
|
return original.apply(this, args);
|
|
49
56
|
};
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
Object.defineProperty(mod, method, { value: wrapper, writable: false, configurable: false });
|
|
60
|
+
} catch (e) {
|
|
61
|
+
mod[method] = wrapper;
|
|
62
|
+
}
|
|
50
63
|
});
|
|
51
64
|
});
|
|
65
|
+
|
|
66
|
+
// 4. Propagate to Worker Threads
|
|
67
|
+
try {
|
|
68
|
+
const { Worker } = require('node:worker_threads');
|
|
69
|
+
const originalWorker = Worker;
|
|
70
|
+
const shieldPath = __filename;
|
|
71
|
+
|
|
72
|
+
const WorkerWrapper = class extends originalWorker {
|
|
73
|
+
constructor(filename, options = {}) {
|
|
74
|
+
options.workerData = options.workerData || {};
|
|
75
|
+
options.execArgv = options.execArgv || [];
|
|
76
|
+
if (!options.execArgv.includes('-r')) {
|
|
77
|
+
options.execArgv.push('-r', shieldPath);
|
|
78
|
+
}
|
|
79
|
+
super(filename, options);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
Object.defineProperty(require('node:worker_threads'), 'Worker', { value: WorkerWrapper, writable: false, configurable: false });
|
|
84
|
+
} catch (e) { }
|
|
85
|
+
|
|
86
|
+
// 5. Undici (Modern Fetch) support
|
|
87
|
+
try {
|
|
88
|
+
const undiciChannel = diagnostics.channel('undici:request:create');
|
|
89
|
+
undiciChannel.subscribe(({ request }) => {
|
|
90
|
+
console.warn(`[ZIFT-SHIELD] 🚀 Undici/Fetch Request: ${request.origin}${request.path}`);
|
|
91
|
+
});
|
|
92
|
+
} catch (e) { }
|
|
52
93
|
}
|
|
53
94
|
|
|
54
95
|
// Auto-activate if required via node -r
|