@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7nsane/zift",
3
- "version": "4.2.0",
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; // Rule not matched
50
- triggers.push(...matchedFacts.map(f => ({ ...f, type: req })));
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) - IMMUTABLE
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
- if (cmdStr.includes('curl') || cmdStr.includes('wget') || cmdStr.includes('| sh') || cmdStr.includes('| bash')) {
29
- console.error(`[ZIFT-SHIELD] ⚠️ CRITICAL: Potential Remote Dropper detected in shell execution!`);
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');