@7nsane/zift 4.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,20 +1,20 @@
1
- # ๐Ÿ›ก๏ธ Zift (v4.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 Deeply Hardened Ecosystem Security Engine for JavaScript.**
7
+ **The Symbolically-Intelligent Ecosystem Security Engine for JavaScript.**
8
8
 
9
- Zift v4.0 is the "Deep Hardening" release, featuring **Immutable Runtime Guards** and **Opaque Payload Detection**, specifically designed to resist active attacker bypasses.
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
- ## ๐Ÿš€ Key Advancements (v4.0.0)
11
+ ## ๐Ÿš€ Key Advancements (v5.0.0)
12
12
 
13
- - **๐Ÿ›ก๏ธ Immutable Zift Shield**: Runtime sinks (`http`, `child_process`) are now immutable. Attackers cannot delete or re-assign them to bypass protection.
14
- - **๐Ÿงฉ Opaque Payload Detection**: Automatically flags compiled native binaries (`.node`) as high-risk.
15
- - **๐Ÿงต Universal Protection**: Zift Shield now automatically propagates into `worker_threads`.
16
- - **๐Ÿ•ต๏ธ Evasion Tracking**: Detects non-deterministic sink construction (e.g., using `Date.now()` or `Math.random()` to hide strings).
17
- - **๐ŸŒ Cross-File Intelligence**: Full multi-pass taint tracking for ESM and CommonJS.
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.
18
18
 
19
19
  ## ๐Ÿ“ฆ Quick Start
20
20
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7nsane/zift",
3
- "version": "4.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
@@ -153,7 +153,6 @@ class ASTCollector {
153
153
  }
154
154
  }
155
155
 
156
- // De-obfuscation Trigger
157
156
  const evaluated = this.tryEvaluate(node, sourceCode);
158
157
  if (evaluated) {
159
158
  if (this.getNetworkType(evaluated) || this.isShellSink(evaluated) || evaluated === 'eval' || evaluated === 'Function') {
@@ -254,6 +253,23 @@ class ASTCollector {
254
253
  }
255
254
  }
256
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
+ }
257
273
  },
258
274
  MemberExpression: (node) => {
259
275
  const objectCode = sourceCode.substring(node.object.start, node.object.end);
@@ -275,14 +291,9 @@ class ASTCollector {
275
291
  }
276
292
  },
277
293
  VariableDeclarator: (node) => {
278
- if (node.init && node.id.type === 'Identifier') {
294
+ if (node.init) {
279
295
  const from = sourceCode.substring(node.init.start, node.init.end);
280
- flows.push({
281
- fromVar: from,
282
- toVar: node.id.name,
283
- file: filePath,
284
- line: node.loc.start.line
285
- });
296
+ this.handlePattern(node.id, from, flows, filePath, node.loc.start.line);
286
297
  }
287
298
  },
288
299
  AssignmentExpression: (node) => {
@@ -306,6 +317,9 @@ class ASTCollector {
306
317
  file: filePath,
307
318
  line: node.loc.start.line
308
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);
309
323
  }
310
324
  },
311
325
  ObjectExpression: (node, state, ancestors) => {
@@ -422,6 +436,28 @@ class ASTCollector {
422
436
  return null;
423
437
  }
424
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
+ }
425
461
  }
426
462
 
427
463
  module.exports = ASTCollector;
package/src/scanner.js CHANGED
@@ -117,13 +117,13 @@ class PackageScanner {
117
117
  facts.EXPORTS.forEach(exp => {
118
118
  if (!exportMap.has(exp.file)) exportMap.set(exp.file, new Map());
119
119
 
120
- // Check if localName is tainted in this file
121
- const isLocalTainted = flows.some(f => f.file === exp.file && f.toVar === exp.local && f.fromVar.includes('process.env'));
122
- 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);
123
122
 
124
123
  exportMap.get(exp.file).set(exp.name, {
125
124
  local: exp.local,
126
- isTainted: isLocalTainted || isNamedTainted
125
+ isTainted: !!resolvedTaint,
126
+ taintPath: resolvedTaint
127
127
  });
128
128
  });
129
129
 
@@ -132,7 +132,7 @@ class PackageScanner {
132
132
  let resolvedPath;
133
133
  if (imp.source.startsWith('.')) {
134
134
  resolvedPath = path.resolve(path.dirname(imp.file), imp.source);
135
- if (!resolvedPath.endsWith('.js')) resolvedPath += '.js';
135
+ if (!resolvedPath.endsWith('.js') && fs.existsSync(resolvedPath + '.js')) resolvedPath += '.js';
136
136
  }
137
137
 
138
138
  if (resolvedPath && exportMap.has(resolvedPath)) {
@@ -140,18 +140,50 @@ class PackageScanner {
140
140
  const matchedExport = targetExports.get(imp.imported);
141
141
 
142
142
  if (matchedExport && matchedExport.isTainted) {
143
- // Mark as a virtual ENV_READ in the importing file
144
143
  facts.ENV_READ.push({
145
144
  file: imp.file,
146
145
  line: imp.line,
147
- variable: `[Cross-File] ${imp.local} (from ${imp.source})`,
146
+ variable: `[Symbolic Cross-File] ${imp.local} <- ${matchedExport.taintPath} (from ${imp.source})`,
148
147
  isCrossFile: true
149
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
+ });
150
158
  }
151
159
  }
152
160
  });
153
161
  }
154
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
+
155
187
  async getFiles() {
156
188
  // Load .ziftignore
157
189
  const ziftIgnorePath = path.join(this.packageDir, '.ziftignore');