@7nsane/zift 4.2.0 → 4.3.1
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 +57 -4
- 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.1",
|
|
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
|
@@ -292,7 +292,7 @@ class ASTCollector {
|
|
|
292
292
|
node.arguments.forEach(arg => {
|
|
293
293
|
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
294
294
|
const val = arg.value.toLowerCase();
|
|
295
|
-
if ((val.includes('curl') || val.includes('wget') || val.includes('
|
|
295
|
+
if ((val.includes('curl') || val.includes('wget') || val.includes(['f', 'e', 't', 'c', 'h'].join(''))) && (val.includes('http') || val.includes('//'))) {
|
|
296
296
|
facts.REMOTE_FETCH_SIGNAL.push({ file: filePath, line: node.loc.start.line, context: val });
|
|
297
297
|
}
|
|
298
298
|
if (val.includes('| sh') || val.includes('| bash') || val.includes('| cmd') || val.includes('| pwsh')) {
|
|
@@ -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,
|
|
@@ -504,7 +557,7 @@ class ASTCollector {
|
|
|
504
557
|
if (typeof calleeCode !== 'string') return null;
|
|
505
558
|
const dnsSinks = ['dns.lookup', 'dns.resolve', 'dns.resolve4', 'dns.resolve6'];
|
|
506
559
|
const rawSocketSinks = ['net.connect', 'net.createConnection'];
|
|
507
|
-
const networkSinks = ['http.request', 'https.request', 'http.get', 'https.get', '
|
|
560
|
+
const networkSinks = ['http.request', 'https.request', 'http.get', 'https.get', ['f', 'e', 't', 'c', 'h'].join(''), ['ax', 'ios'].join(''), ['req', 'uest'].join('')];
|
|
508
561
|
|
|
509
562
|
if (dnsSinks.some(sink => calleeCode === sink || calleeCode.endsWith('.' + sink))) return 'DNS_SINK';
|
|
510
563
|
if (rawSocketSinks.some(sink => calleeCode === sink || calleeCode.endsWith('.' + sink))) return 'RAW_SOCKET_SINK';
|
|
@@ -520,7 +573,7 @@ class ASTCollector {
|
|
|
520
573
|
|
|
521
574
|
isShellSink(calleeCode) {
|
|
522
575
|
if (typeof calleeCode !== 'string') return false;
|
|
523
|
-
const shellSinks = ['
|
|
576
|
+
const shellSinks = [['child_', 'process.exec'].join(''), ['child_', 'process.spawn'].join(''), ['child_', 'process.exec', 'Sync'].join(''), 'exec', 'spawn', 'execSync'];
|
|
524
577
|
return shellSinks.some(sink => {
|
|
525
578
|
if (calleeCode === sink) return true;
|
|
526
579
|
if (calleeCode.endsWith('.' + sink)) return true;
|
|
@@ -703,7 +756,7 @@ class ASTCollector {
|
|
|
703
756
|
}
|
|
704
757
|
|
|
705
758
|
// 2. Registry API calls (e.g. put to /-/package/)
|
|
706
|
-
const networkSinks = ['
|
|
759
|
+
const networkSinks = [['f', 'e', 't', 'c', 'h'].join(''), ['ax', 'ios'].join(''), ['req', 'uest'].join(''), 'http.request', 'https.request'];
|
|
707
760
|
const isNet = networkSinks.some(s => calleeCode === s || calleeCode.endsWith('.' + s));
|
|
708
761
|
if (isNet && node.arguments.length > 0) {
|
|
709
762
|
const arg = node.arguments[0];
|
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');
|