@7nsane/zift 4.2.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/package.json +2 -2
- package/src/collector.js +53 -0
- package/src/engine.js +19 -5
- package/src/shield.js +50 -4
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
|
@@ -427,6 +427,45 @@ class ASTCollector {
|
|
|
427
427
|
}
|
|
428
428
|
});
|
|
429
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
|
+
|
|
430
469
|
// v5.0 Symbolic Transformers: Buffer/Base64/Hex
|
|
431
470
|
if (calleeCode.includes('Buffer.from') || calleeCode.includes('.toString')) {
|
|
432
471
|
const parent = ancestors[ancestors.length - 2];
|
|
@@ -452,6 +491,20 @@ class ASTCollector {
|
|
|
452
491
|
},
|
|
453
492
|
AssignmentExpression: (node) => {
|
|
454
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
|
+
|
|
455
508
|
if (leftCode === 'module.exports' || leftCode.startsWith('exports.')) {
|
|
456
509
|
facts.EXPORTS.push({
|
|
457
510
|
file: filePath,
|
package/src/engine.js
CHANGED
|
@@ -30,24 +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
34
|
|
|
35
35
|
// Handle virtual requirements (LIFECYCLE_CONTEXT)
|
|
36
36
|
if (req === 'LIFECYCLE_CONTEXT' && matchedFacts.length === 0) {
|
|
37
|
-
// If any trigger so far is in a lifecycle file, we satisfy the virtual requirement
|
|
38
37
|
const virtualMatch = triggers.some(t => {
|
|
39
38
|
if (lifecycleFiles instanceof Set) return lifecycleFiles.has(t.file);
|
|
40
39
|
if (Array.isArray(lifecycleFiles)) return lifecycleFiles.includes(t.file);
|
|
41
40
|
return false;
|
|
42
41
|
});
|
|
43
|
-
|
|
44
42
|
if (virtualMatch) {
|
|
45
43
|
matchedFacts = [{ type: 'LIFECYCLE_CONTEXT', virtual: true }];
|
|
46
44
|
}
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
if (matchedFacts.length === 0) return null;
|
|
50
|
-
|
|
47
|
+
if (matchedFacts.length === 0) return null;
|
|
48
|
+
|
|
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
|
+
}
|
|
62
|
+
|
|
63
|
+
if (matchedFacts.length === 0) return null;
|
|
64
|
+
triggers.push(...matchedFacts);
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
// Check optional facts for bonuses
|
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');
|