@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 +9 -9
- package/package.json +1 -1
- package/src/collector.js +44 -8
- package/src/scanner.js +39 -7
package/README.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
# ๐ก๏ธ Zift (v4.
|
|
1
|
+
# ๐ก๏ธ Zift (v4.1.0)
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@7nsane/zift)
|
|
4
4
|
[](https://www.npmjs.com/package/@7nsane/zift)
|
|
5
5
|
[](https://github.com/7nsane/zift)
|
|
6
6
|
|
|
7
|
-
**The
|
|
7
|
+
**The Symbolically-Intelligent Ecosystem Security Engine for JavaScript.**
|
|
8
8
|
|
|
9
|
-
Zift
|
|
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 (
|
|
11
|
+
## ๐ Key Advancements (v5.0.0)
|
|
12
12
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
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
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
|
|
294
|
+
if (node.init) {
|
|
279
295
|
const from = sourceCode.substring(node.init.start, node.init.end);
|
|
280
|
-
flows.
|
|
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
|
|
121
|
-
const
|
|
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:
|
|
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');
|