@7nsane/zift 4.1.0 → 4.3.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 +7 -6
- package/bin/zift.js +8 -1
- package/package.json +2 -2
- package/src/collector.js +353 -24
- package/src/engine.js +33 -9
- package/src/rules/definitions.js +133 -3
- package/src/scanner.js +26 -11
- package/src/shield.js +50 -4
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# 🛡️ Zift (v4.
|
|
1
|
+
# 🛡️ Zift (v4.2.0)
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@7nsane/zift)
|
|
4
4
|
[](https://www.npmjs.com/package/@7nsane/zift)
|
|
@@ -6,15 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
**The Symbolically-Intelligent Ecosystem Security Engine for JavaScript.**
|
|
8
8
|
|
|
9
|
-
Zift
|
|
9
|
+
Zift v4.2 is the "Deep Hardening" release, introducing advanced behavioral rules for Wiper detection, Worm prevention, and OS-specific targeting analysis.
|
|
10
10
|
|
|
11
|
-
## 🚀 Key Advancements (
|
|
11
|
+
## 🚀 Key Advancements (v4.2.0)
|
|
12
12
|
|
|
13
13
|
- **🧠 Symbolic Taint Analysis**: Tracks data through destructuring (`const { key } = process.env`) and deep property access (`obj.a.b.c`).
|
|
14
14
|
- **🧬 Transformation Tracking**: Automatically follows taint through encoding methods like `Buffer.from(data).toString('base64')` or `hex`.
|
|
15
|
-
-
|
|
16
|
-
- **🛡️
|
|
17
|
-
-
|
|
15
|
+
- **🐛 Worm & Propagation Defense**: Detects the chain of credential theft, data exfiltration, and self-publishing (registry hijacking).
|
|
16
|
+
- **🛡️ Deep Behavioral Hardening**: Flags wipers (recursive deletions), CI/CD secret theft, and unauthorized module/git tampering.
|
|
17
|
+
- **📡 OS Fingerprinting Detection**: Identifies system targeting behaviors (os.platform, arch) coupled with network activity.
|
|
18
|
+
- **📦 Lifecycle-Specific Intelligence**: Detects remote fetches and binary drops occurring during sensitive contexts like `preinstall`.
|
|
18
19
|
|
|
19
20
|
## 📦 Quick Start
|
|
20
21
|
|
package/bin/zift.js
CHANGED
|
@@ -28,6 +28,13 @@ async function main() {
|
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
if (args[0] === 'scan') {
|
|
32
|
+
target = args[1] || '.';
|
|
33
|
+
} else if (args[0] && !args[0].startsWith('-')) {
|
|
34
|
+
// If first arg isn't a command or flag, it might be a target
|
|
35
|
+
target = args[0];
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
// 2. Detection for bun/pnpm usage
|
|
32
39
|
if (args.includes('--bun')) installer = 'bun';
|
|
33
40
|
if (args.includes('--pnpm')) installer = 'pnpm';
|
|
@@ -58,7 +65,7 @@ async function main() {
|
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
// 6. Execution
|
|
61
|
-
const isLocal = fs.existsSync(target)
|
|
68
|
+
const isLocal = fs.existsSync(target); // Check existence, dir/file handled by scanner
|
|
62
69
|
|
|
63
70
|
if (isLocal) {
|
|
64
71
|
await runLocalScan(target, format);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@7nsane/zift",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "A high-performance, deterministic security scanner for npm packages.",
|
|
5
5
|
"main": "src/scanner.js",
|
|
6
6
|
"bin": {
|
|
@@ -27,4 +27,4 @@
|
|
|
27
27
|
"chalk": "^4.1.2",
|
|
28
28
|
"glob": "^13.0.6"
|
|
29
29
|
}
|
|
30
|
-
}
|
|
30
|
+
}
|
package/src/collector.js
CHANGED
|
@@ -29,7 +29,19 @@ class ASTCollector {
|
|
|
29
29
|
EXPORTS: [],
|
|
30
30
|
IMPORTS: [],
|
|
31
31
|
OPAQUE_STRING_SKIP: [],
|
|
32
|
-
NON_DETERMINISTIC_SINK: []
|
|
32
|
+
NON_DETERMINISTIC_SINK: [],
|
|
33
|
+
CREDENTIAL_FILE_ACCESS: [],
|
|
34
|
+
DISCORD_STORAGE_ACCESS: [],
|
|
35
|
+
WEBHOOK_SINK: [],
|
|
36
|
+
EVASION_ENVIRONMENT_CHECK: [],
|
|
37
|
+
WALLET_HOOK: [],
|
|
38
|
+
CICD_SECRET_ACCESS: [],
|
|
39
|
+
WIPER_OPERATION: [],
|
|
40
|
+
REGISTRY_TAMPER: [],
|
|
41
|
+
MODULE_TAMPER: [],
|
|
42
|
+
REVERSE_SHELL_BEHAVIOR: [],
|
|
43
|
+
FINGERPRINT_SIGNAL: [],
|
|
44
|
+
PUBLISH_SINK: []
|
|
33
45
|
};
|
|
34
46
|
const flows = [];
|
|
35
47
|
const sourceCode = code;
|
|
@@ -124,6 +136,82 @@ class ASTCollector {
|
|
|
124
136
|
type: 'default'
|
|
125
137
|
});
|
|
126
138
|
},
|
|
139
|
+
Identifier: (node) => {
|
|
140
|
+
const evasionIds = ['v8debug'];
|
|
141
|
+
if (evasionIds.includes(node.name)) {
|
|
142
|
+
facts.EVASION_ENVIRONMENT_CHECK.push({
|
|
143
|
+
file: filePath,
|
|
144
|
+
line: node.loc.start.line,
|
|
145
|
+
context: node.name
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
MemberExpression: (node, state, ancestors) => {
|
|
150
|
+
const memberCode = sourceCode.substring(node.start, node.end);
|
|
151
|
+
|
|
152
|
+
// 1. Anti-Analysis / Evasion Check
|
|
153
|
+
const evasionPatterns = ['debugPort', 'v8debug', 'NODE_OPTIONS'];
|
|
154
|
+
if (evasionPatterns.some(p => memberCode.includes(p))) {
|
|
155
|
+
facts.EVASION_ENVIRONMENT_CHECK.push({
|
|
156
|
+
file: filePath,
|
|
157
|
+
line: node.loc.start.line,
|
|
158
|
+
context: memberCode
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 2. Wallet DRAINER Hook detection
|
|
163
|
+
const walletPatterns = ['ethereum', 'solana', 'phantom'];
|
|
164
|
+
if (walletPatterns.some(p => memberCode.includes(p))) {
|
|
165
|
+
facts.WALLET_HOOK.push({
|
|
166
|
+
file: filePath,
|
|
167
|
+
line: node.loc.start.line,
|
|
168
|
+
context: memberCode
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 3. CI/CD Secret Access
|
|
173
|
+
const cicdPatterns = ['GITHUB_TOKEN', 'CIRCLECI_TOKEN', 'AZURE_TOKEN', 'TRAVIS_TOKEN', 'GITLAB_TOKEN'];
|
|
174
|
+
if (cicdPatterns.some(p => memberCode.includes(p))) {
|
|
175
|
+
facts.CICD_SECRET_ACCESS.push({
|
|
176
|
+
file: filePath,
|
|
177
|
+
line: node.loc.start.line,
|
|
178
|
+
variable: memberCode
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 4. OS Fingerprinting (platform, arch, release)
|
|
183
|
+
const fingerPatterns = ['process.platform', 'process.arch', 'os.platform', 'os.arch', 'os.release', 'os.type'];
|
|
184
|
+
if (fingerPatterns.some(p => memberCode.includes(p))) {
|
|
185
|
+
// Avoid duplicate if it's part of a CallExpression (will be caught there)
|
|
186
|
+
const parent = ancestors[ancestors.length - 2];
|
|
187
|
+
if (parent && parent.type === 'CallExpression' && parent.callee === node) return;
|
|
188
|
+
|
|
189
|
+
facts.FINGERPRINT_SIGNAL.push({
|
|
190
|
+
file: filePath,
|
|
191
|
+
line: node.loc.start.line,
|
|
192
|
+
context: memberCode
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 4. process.env access (Moved from redundant visitor)
|
|
197
|
+
const objectCode = sourceCode.substring(node.object.start, node.object.end);
|
|
198
|
+
if (objectCode === 'process.env' || objectCode === 'process["env"]' || objectCode === "process['env']") {
|
|
199
|
+
const property = node.property.name || (node.property.type === 'Literal' ? node.property.value : null);
|
|
200
|
+
const whitelist = ['NODE_ENV', 'TIMING', 'DEBUG', 'VERBOSE', 'CI', 'APPDATA', 'HOME', 'USERPROFILE', 'PATH', 'PWD'];
|
|
201
|
+
if (whitelist.includes(property)) return;
|
|
202
|
+
|
|
203
|
+
envAccessCount++;
|
|
204
|
+
facts.ENV_READ.push({
|
|
205
|
+
file: filePath,
|
|
206
|
+
line: node.loc.start.line,
|
|
207
|
+
variable: property ? `process.env.${property}` : 'process.env'
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (envAccessCount > 5) {
|
|
211
|
+
facts.MASS_ENV_ACCESS.push({ file: filePath, line: node.loc.start.line, count: envAccessCount });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
127
215
|
CallExpression: (node, state, ancestors) => {
|
|
128
216
|
const calleeCode = sourceCode.substring(node.callee.start, node.callee.end);
|
|
129
217
|
|
|
@@ -175,6 +263,23 @@ class ASTCollector {
|
|
|
175
263
|
line: node.loc.start.line,
|
|
176
264
|
callee: calleeCode
|
|
177
265
|
});
|
|
266
|
+
|
|
267
|
+
// Check for Webhook Sinks
|
|
268
|
+
if (netType === 'NETWORK_SINK') {
|
|
269
|
+
node.arguments.forEach(arg => {
|
|
270
|
+
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
271
|
+
const val = arg.value.toLowerCase();
|
|
272
|
+
const webhooks = ['discord.com/api/webhooks', 'pipedream.net', 'webhook.site', 'burpcollaborator.net'];
|
|
273
|
+
if (webhooks.some(w => val.includes(w))) {
|
|
274
|
+
facts.WEBHOOK_SINK.push({
|
|
275
|
+
file: filePath,
|
|
276
|
+
line: node.loc.start.line,
|
|
277
|
+
url: val
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
178
283
|
}
|
|
179
284
|
|
|
180
285
|
if (this.isShellSink(calleeCode)) {
|
|
@@ -206,10 +311,30 @@ class ASTCollector {
|
|
|
206
311
|
}
|
|
207
312
|
|
|
208
313
|
if (this.isSensitiveFileRead(calleeCode, node, sourceCode)) {
|
|
209
|
-
|
|
314
|
+
const arg = node.arguments[0];
|
|
315
|
+
const pathValue = arg && arg.type === 'Literal' ? String(arg.value).toLowerCase() : '';
|
|
316
|
+
const isCredential = ['.aws', '.ssh', '.npmrc', 'aws_access_key', 'shadow'].some(s => pathValue.includes(s));
|
|
317
|
+
|
|
318
|
+
if (isCredential) {
|
|
319
|
+
facts.CREDENTIAL_FILE_ACCESS.push({
|
|
320
|
+
file: filePath,
|
|
321
|
+
line: node.loc.start.line,
|
|
322
|
+
path: pathValue
|
|
323
|
+
});
|
|
324
|
+
} else {
|
|
325
|
+
facts.FILE_READ_SENSITIVE.push({
|
|
326
|
+
file: filePath,
|
|
327
|
+
line: node.loc.start.line,
|
|
328
|
+
path: sourceCode.substring(node.arguments[0].start, node.arguments[0].end)
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (this.isDiscordStorageAccess(calleeCode, node, sourceCode)) {
|
|
334
|
+
facts.DISCORD_STORAGE_ACCESS.push({
|
|
210
335
|
file: filePath,
|
|
211
336
|
line: node.loc.start.line,
|
|
212
|
-
path:
|
|
337
|
+
path: sourceCode.substring(node.arguments[0].start, node.arguments[0].end)
|
|
213
338
|
});
|
|
214
339
|
}
|
|
215
340
|
|
|
@@ -221,6 +346,54 @@ class ASTCollector {
|
|
|
221
346
|
});
|
|
222
347
|
}
|
|
223
348
|
|
|
349
|
+
if (this.isWiperOperation(calleeCode, node, sourceCode)) {
|
|
350
|
+
facts.WIPER_OPERATION.push({
|
|
351
|
+
file: filePath,
|
|
352
|
+
line: node.loc.start.line,
|
|
353
|
+
path: node.arguments[0] ? sourceCode.substring(node.arguments[0].start, node.arguments[0].end) : 'unknown'
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (this.isRegistryTamper(calleeCode, node, sourceCode)) {
|
|
358
|
+
facts.REGISTRY_TAMPER.push({
|
|
359
|
+
file: filePath,
|
|
360
|
+
line: node.loc.start.line,
|
|
361
|
+
path: node.arguments[0] ? sourceCode.substring(node.arguments[0].start, node.arguments[0].end) : 'unknown'
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (this.isModuleTamper(calleeCode, node, sourceCode)) {
|
|
366
|
+
facts.MODULE_TAMPER.push({
|
|
367
|
+
file: filePath,
|
|
368
|
+
line: node.loc.start.line,
|
|
369
|
+
path: node.arguments[0] ? sourceCode.substring(node.arguments[0].start, node.arguments[0].end) : 'unknown'
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (this.isReverseShellBehavior(calleeCode, node, sourceCode)) {
|
|
374
|
+
facts.REVERSE_SHELL_BEHAVIOR.push({
|
|
375
|
+
file: filePath,
|
|
376
|
+
line: node.loc.start.line,
|
|
377
|
+
context: calleeCode
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (this.isPublishSink(calleeCode, node, sourceCode)) {
|
|
382
|
+
facts.PUBLISH_SINK.push({
|
|
383
|
+
file: filePath,
|
|
384
|
+
line: node.loc.start.line,
|
|
385
|
+
callee: calleeCode
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (this.isOSFingerprint(calleeCode, node, sourceCode)) {
|
|
390
|
+
facts.FINGERPRINT_SIGNAL.push({
|
|
391
|
+
file: filePath,
|
|
392
|
+
line: node.loc.start.line,
|
|
393
|
+
context: calleeCode
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
224
397
|
node.arguments.forEach((arg, index) => {
|
|
225
398
|
const argCode = sourceCode.substring(arg.start, arg.end);
|
|
226
399
|
const isArgTainted = argCode.includes('process.env') || flows.some(f => {
|
|
@@ -254,6 +427,45 @@ class ASTCollector {
|
|
|
254
427
|
}
|
|
255
428
|
});
|
|
256
429
|
|
|
430
|
+
// v5.2 Symbolic Async: await and .then()
|
|
431
|
+
if (calleeCode.includes('.then')) {
|
|
432
|
+
const parts = calleeCode.split('.then');
|
|
433
|
+
const promiseBase = parts[0];
|
|
434
|
+
const isPromiseTainted = flows.some(f => f.toVar === promiseBase) || promiseBase.includes('process.env') || promiseBase.includes('secret');
|
|
435
|
+
|
|
436
|
+
if (isPromiseTainted && node.arguments[0] && (node.arguments[0].type === 'ArrowFunctionExpression' || node.arguments[0].type === 'FunctionExpression')) {
|
|
437
|
+
const param = node.arguments[0].params[0];
|
|
438
|
+
if (param && param.type === 'Identifier') {
|
|
439
|
+
flows.push({
|
|
440
|
+
fromVar: promiseBase,
|
|
441
|
+
toVar: param.name,
|
|
442
|
+
file: filePath,
|
|
443
|
+
line: node.loc.start.line,
|
|
444
|
+
async: true
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// v5.1 Symbolic Mutations: .push(), .concat(), .assign()
|
|
451
|
+
const mutationMethods = ['push', 'unshift', 'concat', 'assign', 'append'];
|
|
452
|
+
if (mutationMethods.some(m => calleeCode.endsWith('.' + m))) {
|
|
453
|
+
const objectName = calleeCode.split('.')[0];
|
|
454
|
+
node.arguments.forEach(arg => {
|
|
455
|
+
const argCode = sourceCode.substring(arg.start, arg.end);
|
|
456
|
+
const isArgTainted = argCode.includes('process.env') || flows.some(f => f.toVar === argCode);
|
|
457
|
+
if (isArgTainted) {
|
|
458
|
+
flows.push({
|
|
459
|
+
fromVar: argCode,
|
|
460
|
+
toVar: objectName,
|
|
461
|
+
file: filePath,
|
|
462
|
+
line: node.loc.start.line,
|
|
463
|
+
mutation: calleeCode
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
257
469
|
// v5.0 Symbolic Transformers: Buffer/Base64/Hex
|
|
258
470
|
if (calleeCode.includes('Buffer.from') || calleeCode.includes('.toString')) {
|
|
259
471
|
const parent = ancestors[ancestors.length - 2];
|
|
@@ -271,25 +483,6 @@ class ASTCollector {
|
|
|
271
483
|
}
|
|
272
484
|
}
|
|
273
485
|
},
|
|
274
|
-
MemberExpression: (node) => {
|
|
275
|
-
const objectCode = sourceCode.substring(node.object.start, node.object.end);
|
|
276
|
-
if (objectCode === 'process.env' || objectCode === 'process["env"]' || objectCode === "process['env']") {
|
|
277
|
-
const property = node.property.name || (node.property.type === 'Literal' ? node.property.value : null);
|
|
278
|
-
const whitelist = ['NODE_ENV', 'TIMING', 'DEBUG', 'VERBOSE', 'CI', 'APPDATA', 'HOME', 'USERPROFILE', 'PATH', 'PWD'];
|
|
279
|
-
if (whitelist.includes(property)) return;
|
|
280
|
-
|
|
281
|
-
envAccessCount++;
|
|
282
|
-
facts.ENV_READ.push({
|
|
283
|
-
file: filePath,
|
|
284
|
-
line: node.loc.start.line,
|
|
285
|
-
variable: property ? `process.env.${property}` : 'process.env'
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
if (envAccessCount > 5) {
|
|
289
|
-
facts.MASS_ENV_ACCESS.push({ file: filePath, line: node.loc.start.line, count: envAccessCount });
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
},
|
|
293
486
|
VariableDeclarator: (node) => {
|
|
294
487
|
if (node.init) {
|
|
295
488
|
const from = sourceCode.substring(node.init.start, node.init.end);
|
|
@@ -298,6 +491,20 @@ class ASTCollector {
|
|
|
298
491
|
},
|
|
299
492
|
AssignmentExpression: (node) => {
|
|
300
493
|
const leftCode = sourceCode.substring(node.left.start, node.left.end);
|
|
494
|
+
if (node.right.type === 'AwaitExpression') {
|
|
495
|
+
const from = sourceCode.substring(node.right.argument.start, node.right.argument.end);
|
|
496
|
+
const isFromTainted = flows.some(f => f.toVar === from) || from.includes('process.env');
|
|
497
|
+
if (isFromTainted) {
|
|
498
|
+
flows.push({
|
|
499
|
+
fromVar: from,
|
|
500
|
+
toVar: leftCode,
|
|
501
|
+
file: filePath,
|
|
502
|
+
line: node.loc.start.line,
|
|
503
|
+
async: true
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
301
508
|
if (leftCode === 'module.exports' || leftCode.startsWith('exports.')) {
|
|
302
509
|
facts.EXPORTS.push({
|
|
303
510
|
file: filePath,
|
|
@@ -403,8 +610,16 @@ class ASTCollector {
|
|
|
403
610
|
|
|
404
611
|
if (node.arguments.length > 0 && node.arguments[0].type === 'Literal') {
|
|
405
612
|
const pathValue = String(node.arguments[0].value);
|
|
406
|
-
const sensitive = ['.ssh', '.env', 'shadow', 'passwd', 'credentials', 'token', '_netrc', 'aws_access_key'];
|
|
407
|
-
|
|
613
|
+
const sensitive = ['.ssh', '.env', 'shadow', 'passwd', 'credentials', 'token', '_netrc', 'aws_access_key', '.npmrc'];
|
|
614
|
+
if (sensitive.some((s) => pathValue.includes(s))) return true;
|
|
615
|
+
|
|
616
|
+
// Deep check: if argument is a variable, check if it was initialized with a sensitive string
|
|
617
|
+
const arg = node.arguments[0];
|
|
618
|
+
if (arg && arg.type === 'Identifier') {
|
|
619
|
+
const varName = arg.name;
|
|
620
|
+
// This is a bit complex for a one-liner, but we can check if it's in our local flows
|
|
621
|
+
// For now, let's just stick to the code string includes, which already handles BinaryExpressions of literals
|
|
622
|
+
}
|
|
408
623
|
}
|
|
409
624
|
return false;
|
|
410
625
|
}
|
|
@@ -422,6 +637,21 @@ class ASTCollector {
|
|
|
422
637
|
return false;
|
|
423
638
|
}
|
|
424
639
|
|
|
640
|
+
isDiscordStorageAccess(calleeCode, node, sourceCode) {
|
|
641
|
+
if (typeof calleeCode !== 'string') return false;
|
|
642
|
+
if (!calleeCode.includes('fs.readFile') && !calleeCode.includes('fs.readFileSync')) return false;
|
|
643
|
+
|
|
644
|
+
if (node.arguments.length > 0) {
|
|
645
|
+
const arg = node.arguments[0];
|
|
646
|
+
const argCode = sourceCode.substring(arg.start, arg.end).toLowerCase();
|
|
647
|
+
|
|
648
|
+
// Detection: check if the argument code OR any part of the expression contains 'discord' and 'local storage'
|
|
649
|
+
// We also check identifiers if their names are suspicious (simple heuristic)
|
|
650
|
+
return argCode.includes('discord') && (argCode.includes('local storage') || argCode.includes('leveldb') || argCode.includes('token'));
|
|
651
|
+
}
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
|
|
425
655
|
tryEvaluate(node, sourceCode) {
|
|
426
656
|
try {
|
|
427
657
|
const code = sourceCode.substring(node.start, node.end);
|
|
@@ -458,6 +688,105 @@ class ASTCollector {
|
|
|
458
688
|
});
|
|
459
689
|
}
|
|
460
690
|
}
|
|
691
|
+
|
|
692
|
+
isWiperOperation(calleeCode, node, sourceCode) {
|
|
693
|
+
if (typeof calleeCode !== 'string') return false;
|
|
694
|
+
const wiperFuncs = ['fs.rm', 'fs.rmSync', 'fs.rmdir', 'fs.rmdirSync', 'fs.unlink', 'fs.unlinkSync'];
|
|
695
|
+
const isWiperFunc = wiperFuncs.some(f => calleeCode === f || calleeCode.endsWith('.' + f));
|
|
696
|
+
if (!isWiperFunc) return false;
|
|
697
|
+
|
|
698
|
+
const arg = node.arguments[0];
|
|
699
|
+
if (!arg) return false;
|
|
700
|
+
|
|
701
|
+
const argCode = sourceCode.substring(arg.start, arg.end).toLowerCase();
|
|
702
|
+
const sensitivePaths = ['/root', '/home', '/etc', '/var/log', '/usr/bin', '/bin', 'c:\\windows', 'c:\\users'];
|
|
703
|
+
const isSensitivePath = sensitivePaths.some(p => argCode.includes(p));
|
|
704
|
+
|
|
705
|
+
const hasRecursive = sourceCode.substring(node.start, node.end).includes('recursive: true');
|
|
706
|
+
return isSensitivePath || hasRecursive;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
isRegistryTamper(calleeCode, node, sourceCode) {
|
|
710
|
+
if (typeof calleeCode !== 'string') return false;
|
|
711
|
+
if (!calleeCode.includes('fs.writeFile') && !calleeCode.includes('fs.writeFileSync') && !calleeCode.includes('fs.appendFile')) return false;
|
|
712
|
+
|
|
713
|
+
if (node.arguments.length > 0) {
|
|
714
|
+
const arg = node.arguments[0];
|
|
715
|
+
const argCode = sourceCode.substring(arg.start, arg.end).toLowerCase();
|
|
716
|
+
return argCode.includes('.npmrc') || argCode.includes('registry') || argCode.includes('npm-registry');
|
|
717
|
+
}
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
isModuleTamper(calleeCode, node, sourceCode) {
|
|
722
|
+
if (typeof calleeCode !== 'string') return false;
|
|
723
|
+
if (!calleeCode.includes('fs.writeFile') && !calleeCode.includes('fs.writeFileSync') && !calleeCode.includes('fs.mkdir')) return false;
|
|
724
|
+
|
|
725
|
+
if (node.arguments.length > 0) {
|
|
726
|
+
const arg = node.arguments[0];
|
|
727
|
+
const argCode = sourceCode.substring(arg.start, arg.end).toLowerCase();
|
|
728
|
+
return argCode.includes('node_modules') || argCode.includes('.git');
|
|
729
|
+
}
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
isReverseShellBehavior(calleeCode, node, sourceCode) {
|
|
734
|
+
if (typeof calleeCode !== 'string') return false;
|
|
735
|
+
if (calleeCode.endsWith('.pipe')) {
|
|
736
|
+
const arg = node.arguments[0];
|
|
737
|
+
if (arg) {
|
|
738
|
+
const argCode = sourceCode.substring(arg.start, arg.end);
|
|
739
|
+
return ['process.stdin', 'process.stdout', 'sh', 'bash', 'cmd', 'pwsh'].some(s => argCode.includes(s));
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
isPublishSink(calleeCode, node, sourceCode) {
|
|
746
|
+
if (typeof calleeCode !== 'string') return false;
|
|
747
|
+
|
|
748
|
+
// 1. Direct Shell Commands
|
|
749
|
+
if (this.isShellSink(calleeCode)) {
|
|
750
|
+
const arg = node.arguments[0];
|
|
751
|
+
if (arg && arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
752
|
+
const val = arg.value.toLowerCase();
|
|
753
|
+
if (val.includes('npm publish') || val.includes('npm login') || val.includes('npm adduser')) return true;
|
|
754
|
+
if (val.includes('pnpm publish') || val.includes('yarn publish')) return true;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// 2. Registry API calls (e.g. put to /-/package/)
|
|
759
|
+
const networkSinks = ['fetch', 'axios', 'request', 'http.request', 'https.request'];
|
|
760
|
+
const isNet = networkSinks.some(s => calleeCode === s || calleeCode.endsWith('.' + s));
|
|
761
|
+
if (isNet && node.arguments.length > 0) {
|
|
762
|
+
const arg = node.arguments[0];
|
|
763
|
+
if (arg && arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
764
|
+
const val = arg.value.toLowerCase();
|
|
765
|
+
if (val.includes('registry.npmjs.org') && (val.includes('/-/user/') || val.includes('/-/package/'))) return true;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
isOSFingerprint(calleeCode, node, sourceCode) {
|
|
773
|
+
if (typeof calleeCode !== 'string') return false;
|
|
774
|
+
|
|
775
|
+
// 1. OS Module Methods
|
|
776
|
+
const osMethods = ['os.platform', 'os.arch', 'os.release', 'os.type', 'os.cpus', 'os.networkInterfaces', 'os.userInfo'];
|
|
777
|
+
if (osMethods.some(m => calleeCode === m || calleeCode.endsWith('.' + m))) return true;
|
|
778
|
+
|
|
779
|
+
// 2. File Reads of OS metadata
|
|
780
|
+
if (calleeCode.includes('fs.readFile') || calleeCode.includes('fs.readFileSync')) {
|
|
781
|
+
const arg = node.arguments[0];
|
|
782
|
+
if (arg && arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
783
|
+
const val = arg.value.toLowerCase();
|
|
784
|
+
if (val.includes('/etc/os-release') || val.includes('/etc/issue') || val.includes('/proc/version')) return true;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
461
790
|
}
|
|
462
791
|
|
|
463
792
|
module.exports = ASTCollector;
|
package/src/engine.js
CHANGED
|
@@ -30,14 +30,38 @@ class SafetyEngine {
|
|
|
30
30
|
|
|
31
31
|
// Check required facts
|
|
32
32
|
for (const req of rule.requires) {
|
|
33
|
-
let matchedFacts = facts[req] || [];
|
|
33
|
+
let matchedFacts = (facts[req] || []).map(f => ({ ...f, type: req }));
|
|
34
|
+
|
|
35
|
+
// Handle virtual requirements (LIFECYCLE_CONTEXT)
|
|
36
|
+
if (req === 'LIFECYCLE_CONTEXT' && matchedFacts.length === 0) {
|
|
37
|
+
const virtualMatch = triggers.some(t => {
|
|
38
|
+
if (lifecycleFiles instanceof Set) return lifecycleFiles.has(t.file);
|
|
39
|
+
if (Array.isArray(lifecycleFiles)) return lifecycleFiles.includes(t.file);
|
|
40
|
+
return false;
|
|
41
|
+
});
|
|
42
|
+
if (virtualMatch) {
|
|
43
|
+
matchedFacts = [{ type: 'LIFECYCLE_CONTEXT', virtual: true }];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (matchedFacts.length === 0) return null;
|
|
34
48
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
// v5.3 Sequence Matching: Ensure facts occur in specified order (if rule has .sequence)
|
|
50
|
+
if (rule.sequence) {
|
|
51
|
+
const reqIndex = rule.requires.indexOf(req);
|
|
52
|
+
if (reqIndex > 0) {
|
|
53
|
+
const prevReq = rule.requires[reqIndex - 1];
|
|
54
|
+
const prevTriggers = triggers.filter(t => t.type === prevReq);
|
|
55
|
+
|
|
56
|
+
// Filter current matches to only those that happen AFTER a previous trigger
|
|
57
|
+
matchedFacts = matchedFacts.filter(curr => {
|
|
58
|
+
return prevTriggers.some(prev => curr.line >= prev.line);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
38
62
|
|
|
39
|
-
if (matchedFacts.length === 0) return null;
|
|
40
|
-
triggers.push(...matchedFacts
|
|
63
|
+
if (matchedFacts.length === 0) return null;
|
|
64
|
+
triggers.push(...matchedFacts);
|
|
41
65
|
}
|
|
42
66
|
|
|
43
67
|
// Check optional facts for bonuses
|
|
@@ -65,7 +89,7 @@ class SafetyEngine {
|
|
|
65
89
|
|
|
66
90
|
// Cluster Bonus: Source + Sink
|
|
67
91
|
const hasSource = triggers.some(t => t.type.includes('READ') || t.type.includes('ACCESS'));
|
|
68
|
-
const hasSink = triggers.some(t => t.type.includes('SINK') || t.type === 'DYNAMIC_EXECUTION' || t.type === 'SHELL_EXECUTION' || t.type === 'DYNAMIC_REQUIRE');
|
|
92
|
+
const hasSink = triggers.some(t => t.type.includes('SINK') || t.type === 'DYNAMIC_EXECUTION' || t.type === 'SHELL_EXECUTION' || t.type === 'DYNAMIC_REQUIRE' || t.type === 'WIPER_OPERATION' || t.type === 'REVERSE_SHELL_BEHAVIOR');
|
|
69
93
|
if (hasSource && hasSink) {
|
|
70
94
|
baseScore += 40;
|
|
71
95
|
}
|
|
@@ -73,8 +97,8 @@ class SafetyEngine {
|
|
|
73
97
|
let finalScore = baseScore * multiplier;
|
|
74
98
|
|
|
75
99
|
// Severe Cluster: SENSITIVE_READ + Dangerous Sink + lifecycleContext = Critical (100)
|
|
76
|
-
const isSensitiveRead = triggers.some(t => t.type === 'ENV_READ' || t.type === 'FILE_READ_SENSITIVE');
|
|
77
|
-
const isDangerousSink = triggers.some(t => t.type === 'NETWORK_SINK' || t.type === 'DNS_SINK' || t.type === 'RAW_SOCKET_SINK' || t.type === 'SHELL_EXECUTION');
|
|
100
|
+
const isSensitiveRead = triggers.some(t => t.type === 'ENV_READ' || t.type === 'FILE_READ_SENSITIVE' || t.type === 'CICD_SECRET_ACCESS');
|
|
101
|
+
const isDangerousSink = triggers.some(t => t.type === 'NETWORK_SINK' || t.type === 'DNS_SINK' || t.type === 'RAW_SOCKET_SINK' || t.type === 'SHELL_EXECUTION' || t.type === 'WEBHOOK_SINK' || t.type === 'WIPER_OPERATION' || t.type === 'REVERSE_SHELL_BEHAVIOR');
|
|
78
102
|
if (isSensitiveRead && isDangerousSink && isInLifecycle) {
|
|
79
103
|
finalScore = 100;
|
|
80
104
|
}
|
package/src/rules/definitions.js
CHANGED
|
@@ -137,13 +137,143 @@ const RULES = [
|
|
|
137
137
|
priority: 1,
|
|
138
138
|
baseScore: 25,
|
|
139
139
|
description: 'Detection of very large high-entropy strings that exceed scanning limits.'
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'ZFT-016',
|
|
143
|
+
alias: 'CREDENTIAL_STEAL_ATTEMPT',
|
|
144
|
+
name: 'Credential Theft Attempt',
|
|
145
|
+
requires: ['CREDENTIAL_FILE_ACCESS', 'NETWORK_SINK'],
|
|
146
|
+
priority: 1,
|
|
147
|
+
baseScore: 85,
|
|
148
|
+
description: 'Detection of access to sensitive credential files (.aws, .ssh, .npmrc) and exfiltration.'
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'ZFT-017',
|
|
152
|
+
alias: 'ANTI_ANALYSIS_EVASION',
|
|
153
|
+
name: 'Anti-Analysis / Evasion',
|
|
154
|
+
requires: ['EVASION_ENVIRONMENT_CHECK'],
|
|
155
|
+
optional: ['DYNAMIC_EXECUTION'],
|
|
156
|
+
priority: 2,
|
|
157
|
+
baseScore: 50,
|
|
158
|
+
description: 'Detection of code that checks for VM, Debugger, or Sandbox environments to evade analysis.'
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'ZFT-018',
|
|
162
|
+
alias: 'CRYPTO_WALLET_DRAINER',
|
|
163
|
+
name: 'Crypto-Wallet Drainer Hook',
|
|
164
|
+
requires: ['WALLET_HOOK'],
|
|
165
|
+
priority: 1,
|
|
166
|
+
baseScore: 95,
|
|
167
|
+
description: 'Detection of hooks on browser-based crypto wallets (window.ethereum, Solana).'
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'ZFT-019',
|
|
171
|
+
alias: 'DISCORD_TOKEN_STEALER',
|
|
172
|
+
name: 'Discord Token Stealer',
|
|
173
|
+
requires: ['DISCORD_STORAGE_ACCESS', 'NETWORK_SINK'],
|
|
174
|
+
priority: 1,
|
|
175
|
+
baseScore: 80,
|
|
176
|
+
description: 'Detection of Discord local storage access followed by network activity.'
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'ZFT-020',
|
|
180
|
+
alias: 'HIGH_RISK_WEBHOOK_SINK',
|
|
181
|
+
name: 'High-Risk Webhook Exfiltration',
|
|
182
|
+
requires: ['WEBHOOK_SINK'],
|
|
183
|
+
optional: ['ENV_READ', 'ENCODER_USE'],
|
|
184
|
+
priority: 2,
|
|
185
|
+
baseScore: 60,
|
|
186
|
+
description: 'Detection of data being sent to known high-risk exfiltration domains (Discord Webhooks, Pipedream).'
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'ZFT-021',
|
|
190
|
+
alias: 'WIPER_MODULE_DETECTED',
|
|
191
|
+
name: 'Destructive Wiper Module',
|
|
192
|
+
requires: ['WIPER_OPERATION'],
|
|
193
|
+
priority: 1,
|
|
194
|
+
baseScore: 100,
|
|
195
|
+
description: 'Detection of recursive deletion operations on sensitive directory structures (Home, Root, Documents).'
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: 'ZFT-022',
|
|
199
|
+
alias: 'CICD_SECRET_EXFILTRATION',
|
|
200
|
+
name: 'CI/CD Secret Exfiltration',
|
|
201
|
+
requires: ['CICD_SECRET_ACCESS', 'NETWORK_SINK'],
|
|
202
|
+
priority: 1,
|
|
203
|
+
baseScore: 90,
|
|
204
|
+
description: 'Detection of CI/CD secrets (GITHUB_TOKEN, CIRCLECI_TOKEN) being accessed and exfiltrated.'
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: 'ZFT-023',
|
|
208
|
+
alias: 'REGISTRY_POISONING_ATTEMPT',
|
|
209
|
+
name: 'Registry Poisoning Attempt',
|
|
210
|
+
requires: ['REGISTRY_TAMPER'],
|
|
211
|
+
priority: 2,
|
|
212
|
+
baseScore: 70,
|
|
213
|
+
description: 'Detection of unauthorized modifications to .npmrc or registry configuration.'
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 'ZFT-024',
|
|
217
|
+
alias: 'MODULE_REPOS_HIJACKING',
|
|
218
|
+
name: 'Module/Repository Hijacking',
|
|
219
|
+
requires: ['MODULE_TAMPER'],
|
|
220
|
+
priority: 2,
|
|
221
|
+
baseScore: 75,
|
|
222
|
+
description: 'Detection of unauthorized write operations into node_modules or .git directories.'
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: 'ZFT-025',
|
|
226
|
+
alias: 'REVERSE_SHELL_PATTERN',
|
|
227
|
+
name: 'Reverse Shell Behavior',
|
|
228
|
+
requires: ['REVERSE_SHELL_BEHAVIOR'],
|
|
229
|
+
priority: 1,
|
|
230
|
+
baseScore: 95,
|
|
231
|
+
description: 'Detection of network sockets being piped directly into system shells (Reverse Shell pattern).'
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: 'ZFT-026',
|
|
235
|
+
alias: 'REGISTRY_PUBLISH_ATTEMPT',
|
|
236
|
+
name: 'Registry Publication Attempt',
|
|
237
|
+
requires: ['PUBLISH_SINK'],
|
|
238
|
+
priority: 1,
|
|
239
|
+
baseScore: 85,
|
|
240
|
+
description: 'Detection of attempts to run npm publish or interact with registry upload APIs (Worm behavior).'
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
id: 'ZFT-027',
|
|
244
|
+
alias: 'FINGERPRINT_OS_TARGETING',
|
|
245
|
+
name: 'OS Fingerprinting & Targeting',
|
|
246
|
+
requires: ['FINGERPRINT_SIGNAL'],
|
|
247
|
+
optional: ['NETWORK_SINK', 'SHELL_EXECUTION'],
|
|
248
|
+
priority: 2,
|
|
249
|
+
baseScore: 55,
|
|
250
|
+
description: 'Detection of OS metadata collection (platform, release, arch) potentially for targeted payload delivery.'
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
id: 'ZFT-028',
|
|
254
|
+
alias: 'WORM_PROPAGATION_CHAIN',
|
|
255
|
+
name: 'Automated Worm Propagation',
|
|
256
|
+
requires: ['CREDENTIAL_FILE_ACCESS', 'NETWORK_SINK', 'PUBLISH_SINK'],
|
|
257
|
+
priority: 1,
|
|
258
|
+
baseScore: 100,
|
|
259
|
+
description: 'Detection of the full worm cycle: Harvest tokens -> Exfiltrate -> Self-publish.'
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: 'ZFT-029',
|
|
263
|
+
alias: 'LIFECYCLE_BINARY_FETCH',
|
|
264
|
+
name: 'Lifecycle Binary Drop',
|
|
265
|
+
requires: ['REMOTE_FETCH_SIGNAL', 'LIFECYCLE_CONTEXT'],
|
|
266
|
+
optional: ['SHELL_EXECUTION'],
|
|
267
|
+
priority: 1,
|
|
268
|
+
baseScore: 90,
|
|
269
|
+
description: 'Detection of remote payload fetching specifically during package install/lifecycle scripts.'
|
|
140
270
|
}
|
|
141
271
|
];
|
|
142
272
|
|
|
143
273
|
const CATEGORIES = {
|
|
144
|
-
SOURCES: ['ENV_READ', 'FILE_READ_SENSITIVE', 'MASS_ENV_ACCESS'],
|
|
145
|
-
SINKS: ['NETWORK_SINK', 'DNS_SINK', 'RAW_SOCKET_SINK', 'DYNAMIC_EXECUTION', 'SHELL_EXECUTION', 'DYNAMIC_REQUIRE'],
|
|
146
|
-
SIGNALS: ['OBFUSCATION', 'ENCODER_USE', 'REMOTE_FETCH_SIGNAL', 'PIPE_TO_SHELL_SIGNAL', 'NATIVE_BINARY_DETECTED', 'OPAQUE_STRING_SKIP', 'NON_DETERMINISTIC_SINK'],
|
|
274
|
+
SOURCES: ['ENV_READ', 'FILE_READ_SENSITIVE', 'MASS_ENV_ACCESS', 'CREDENTIAL_FILE_ACCESS', 'DISCORD_STORAGE_ACCESS', 'CICD_SECRET_ACCESS'],
|
|
275
|
+
SINKS: ['NETWORK_SINK', 'DNS_SINK', 'RAW_SOCKET_SINK', 'DYNAMIC_EXECUTION', 'SHELL_EXECUTION', 'DYNAMIC_REQUIRE', 'WEBHOOK_SINK', 'WIPER_OPERATION', 'REGISTRY_TAMPER', 'MODULE_TAMPER', 'REVERSE_SHELL_BEHAVIOR', 'PUBLISH_SINK'],
|
|
276
|
+
SIGNALS: ['OBFUSCATION', 'ENCODER_USE', 'REMOTE_FETCH_SIGNAL', 'PIPE_TO_SHELL_SIGNAL', 'NATIVE_BINARY_DETECTED', 'OPAQUE_STRING_SKIP', 'NON_DETERMINISTIC_SINK', 'EVASION_ENVIRONMENT_CHECK', 'WALLET_HOOK', 'FINGERPRINT_SIGNAL'],
|
|
147
277
|
PERSISTENCE: ['FILE_WRITE_STARTUP'],
|
|
148
278
|
CONTEXT: ['LIFECYCLE_CONTEXT']
|
|
149
279
|
};
|
package/src/scanner.js
CHANGED
|
@@ -45,7 +45,19 @@ class PackageScanner {
|
|
|
45
45
|
EXPORTS: [],
|
|
46
46
|
IMPORTS: [],
|
|
47
47
|
NATIVE_BINARY_DETECTED: [],
|
|
48
|
-
OPAQUE_STRING_SKIP: []
|
|
48
|
+
OPAQUE_STRING_SKIP: [],
|
|
49
|
+
CREDENTIAL_FILE_ACCESS: [],
|
|
50
|
+
DISCORD_STORAGE_ACCESS: [],
|
|
51
|
+
WEBHOOK_SINK: [],
|
|
52
|
+
EVASION_ENVIRONMENT_CHECK: [],
|
|
53
|
+
WALLET_HOOK: [],
|
|
54
|
+
CICD_SECRET_ACCESS: [],
|
|
55
|
+
WIPER_OPERATION: [],
|
|
56
|
+
REGISTRY_TAMPER: [],
|
|
57
|
+
MODULE_TAMPER: [],
|
|
58
|
+
REVERSE_SHELL_BEHAVIOR: [],
|
|
59
|
+
FINGERPRINT_SIGNAL: [],
|
|
60
|
+
PUBLISH_SINK: []
|
|
49
61
|
},
|
|
50
62
|
flows: []
|
|
51
63
|
};
|
|
@@ -193,23 +205,26 @@ class PackageScanner {
|
|
|
193
205
|
ignoreLines = [...ignoreLines, ...content.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'))];
|
|
194
206
|
}
|
|
195
207
|
|
|
196
|
-
const getJsFiles = (
|
|
208
|
+
const getJsFiles = (dirOrFile) => {
|
|
209
|
+
const stats = fs.statSync(dirOrFile);
|
|
210
|
+
if (!stats.isDirectory()) {
|
|
211
|
+
if (dirOrFile.endsWith('.js') || dirOrFile.endsWith('.node')) {
|
|
212
|
+
return [dirOrFile];
|
|
213
|
+
}
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
|
|
197
217
|
const results = [];
|
|
198
|
-
const list = fs.readdirSync(
|
|
218
|
+
const list = fs.readdirSync(dirOrFile);
|
|
199
219
|
for (const file of list) {
|
|
200
|
-
const fullPath = path.join(
|
|
220
|
+
const fullPath = path.join(dirOrFile, file);
|
|
201
221
|
const relativePath = path.relative(this.packageDir, fullPath);
|
|
202
222
|
|
|
203
223
|
// Simple ignore check
|
|
204
224
|
if (ignoreLines.some(pattern => relativePath.includes(pattern) || file === pattern)) continue;
|
|
205
225
|
if (file.startsWith('.') && file !== '.ziftignore') continue;
|
|
206
226
|
|
|
207
|
-
|
|
208
|
-
if (stat && stat.isDirectory()) {
|
|
209
|
-
results.push(...getJsFiles(fullPath));
|
|
210
|
-
} else if (file.endsWith('.js') || file.endsWith('.node')) {
|
|
211
|
-
results.push(fullPath);
|
|
212
|
-
}
|
|
227
|
+
results.push(...getJsFiles(fullPath));
|
|
213
228
|
}
|
|
214
229
|
return results;
|
|
215
230
|
};
|
|
@@ -233,7 +248,7 @@ class PackageScanner {
|
|
|
233
248
|
type: t.type,
|
|
234
249
|
file: path.relative(this.packageDir, t.file),
|
|
235
250
|
line: t.line,
|
|
236
|
-
context: t.reason || t.callee || t.variable || t.path
|
|
251
|
+
context: t.reason || t.callee || t.variable || t.path || t.url || t.context
|
|
237
252
|
}))
|
|
238
253
|
};
|
|
239
254
|
}),
|
package/src/shield.js
CHANGED
|
@@ -14,8 +14,10 @@ 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) - ACTIVE BLOCKING
|
|
18
18
|
const cp = require('node:child_process');
|
|
19
|
+
const ALLOWED_COMMANDS = ['npm install', 'npm audit', 'ls', 'dir', 'whoami', 'node -v'];
|
|
20
|
+
|
|
19
21
|
['exec', 'spawn', 'execSync', 'spawnSync'].forEach(method => {
|
|
20
22
|
const original = cp[method];
|
|
21
23
|
if (!original) return;
|
|
@@ -23,10 +25,18 @@ function setupShield() {
|
|
|
23
25
|
const wrapper = function (...args) {
|
|
24
26
|
const command = args[0];
|
|
25
27
|
const cmdStr = typeof command === 'string' ? command : (Array.isArray(args[1]) ? args[1].join(' ') : String(command));
|
|
26
|
-
console.warn(`[ZIFT-SHIELD] 🐚 Shell Execution: ${cmdStr}`);
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
// Security Logic
|
|
30
|
+
const isCritical = cmdStr.includes('curl') || cmdStr.includes('wget') || cmdStr.includes('| sh') || cmdStr.includes('| bash') || cmdStr.includes('rm -rf /');
|
|
31
|
+
const isBlocked = !ALLOWED_COMMANDS.some(allowed => cmdStr.startsWith(allowed)) || isCritical;
|
|
32
|
+
|
|
33
|
+
if (isBlocked) {
|
|
34
|
+
console.error(`[ZIFT-SHIELD] ❌ BLOCKED: Unauthorized or dangerous shell execution: "${cmdStr}"`);
|
|
35
|
+
if (process.env.ZIFT_ENFORCE === 'true') {
|
|
36
|
+
throw new Error(`[ZIFT-SHIELD] Access Denied: Shell command "${cmdStr}" is not in the allow-list.`);
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
console.warn(`[ZIFT-SHIELD] 🐚 Shell Execution (Allowed): ${cmdStr}`);
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
return original.apply(this, args);
|
|
@@ -39,6 +49,42 @@ function setupShield() {
|
|
|
39
49
|
}
|
|
40
50
|
});
|
|
41
51
|
|
|
52
|
+
// 2.5 Filesystem Protection
|
|
53
|
+
const fs = require('node:fs');
|
|
54
|
+
const PROTECTED_FILES = ['.env', '.npmrc', 'shadow', 'id_rsa', 'id_ed25519'];
|
|
55
|
+
|
|
56
|
+
const fsMethods = ['readFile', 'readFileSync', 'promises.readFile', 'createReadStream'];
|
|
57
|
+
fsMethods.forEach(methodPath => {
|
|
58
|
+
let parent = fs;
|
|
59
|
+
let method = methodPath;
|
|
60
|
+
if (methodPath.startsWith('promises.')) {
|
|
61
|
+
parent = fs.promises;
|
|
62
|
+
method = 'readFile';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const original = parent[method];
|
|
66
|
+
if (!original) return;
|
|
67
|
+
|
|
68
|
+
const wrapper = function (...args) {
|
|
69
|
+
const pathArg = args[0];
|
|
70
|
+
const pathStr = typeof pathArg === 'string' ? pathArg : (pathArg instanceof Buffer ? pathArg.toString() : String(pathArg));
|
|
71
|
+
|
|
72
|
+
if (PROTECTED_FILES.some(f => pathStr.includes(f))) {
|
|
73
|
+
console.error(`[ZIFT-SHIELD] ❌ BLOCKED: Access to protected file: "${pathStr}"`);
|
|
74
|
+
if (process.env.ZIFT_ENFORCE === 'true') {
|
|
75
|
+
throw new Error(`[ZIFT-SHIELD] Access Denied: Protected file "${pathStr}" cannot be read.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return original.apply(this, args);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
Object.defineProperty(parent, method, { value: wrapper, writable: false, configurable: false });
|
|
83
|
+
} catch (e) {
|
|
84
|
+
parent[method] = wrapper;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
42
88
|
// 3. Monitor HTTP/HTTPS - IMMUTABLE
|
|
43
89
|
const http = require('node:http');
|
|
44
90
|
const https = require('node:https');
|