@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 CHANGED
@@ -1,19 +1,20 @@
1
- # 🛡️ Zift (v3.0.0)
1
+ # 🛡️ Zift (v4.1.0)
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@7nsane/zift.svg?style=flat-square)](https://www.npmjs.com/package/@7nsane/zift)
4
4
  [![License](https://img.shields.io/npm/l/@7nsane/zift.svg?style=flat-square)](https://www.npmjs.com/package/@7nsane/zift)
5
5
  [![Build Status](https://img.shields.io/badge/CI-passing-brightgreen?style=flat-square)](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 v3.0 is a massive leap forward, moving beyond static analysis into **Cross-File Intelligence** and **Runtime Protection**. It is designed to identify and stop advanced supply-chain attacks (credential exfiltration, reverse-shell droppers) before they hit your production environment.
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
- ## 🚀 Major Features (v3.0.0)
11
+ ## 🚀 Key Advancements (v5.0.0)
12
12
 
13
- - **🌍 Cross-File Taint Tracking**: Tracks sensitive data (e.g., `process.env.TOKEN`) across `import/export` and `require` boundaries.
14
- - **🧠 VM-Based De-obfuscation**: Safe, sandboxed evaluation of string manipulation logic (e.g., character arrays, reverse/join) to reveal hidden malicious signals.
15
- - **🛡️ Zift Shield (Runtime Guard)**: A real-time audit layer for network and shell activity. Run `zift protect` to monitor your app's dependencies in real-world conditions.
16
- - **🔒 Lockfile Security**: Automatic auditing of `package-lock.json` and `yarn.lock` for registry confusion.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7nsane/zift",
3
- "version": "3.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "A high-performance, deterministic security scanner for npm packages.",
5
5
  "main": "src/scanner.js",
6
6
  "bin": {
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 && node.value.length < this.maxStringLengthForEntropy) {
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 && node.id.type === 'Identifier') {
294
+ if (node.init) {
252
295
  const from = sourceCode.substring(node.init.start, node.init.end);
253
- flows.push({
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;
@@ -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 in this file
112
- const isLocalTainted = flows.some(f => f.file === exp.file && f.toVar === exp.local && f.fromVar.includes('process.env'));
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: isLocalTainted || isNamedTainted
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
- cp[method] = function (...args) {
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(' ') : 'unknown');
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
- // Heuristic Check: Is it a potential dropper?
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
- mod[method] = function (...args) {
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