@7nsane/zift 2.2.1 → 3.0.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 +29 -80
- package/bin/zift.js +20 -1
- package/package.json +1 -1
- package/src/collector.js +116 -10
- package/src/scanner.js +53 -13
- package/src/shield.js +57 -0
package/README.md
CHANGED
|
@@ -1,103 +1,52 @@
|
|
|
1
|
-
# 🛡️ Zift (
|
|
1
|
+
# 🛡️ Zift (v3.0.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 Intelligent Ecosystem Security Engine for JavaScript.**
|
|
8
8
|
|
|
9
|
-
|
|
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.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
# Install globally to use the 'zift' command anywhere
|
|
13
|
-
npm install -g @7nsane/zift
|
|
14
|
-
```
|
|
11
|
+
## 🚀 Major Features (v3.0.0)
|
|
15
12
|
|
|
16
|
-
|
|
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.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
## 📦 Quick Start
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
# 1.
|
|
22
|
-
zift
|
|
23
|
-
|
|
24
|
-
# 2. Reload your terminal (or run the command provided by setup)
|
|
25
|
-
|
|
26
|
-
# 3. Use the --zift flag with your normal npm commands
|
|
27
|
-
npm install <package-name> --zift
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## 🔍 Limitations & Blind Spots (v2.0.0)
|
|
31
|
-
|
|
32
|
-
To maintain a zero-false-positive baseline and high performance, Zift v2 focus on **deterministic behavioral patterns**. It does NOT currently cover:
|
|
33
|
-
|
|
34
|
-
- **Cross-file Taint**: Taint tracking is limited to intra-file propagation.
|
|
35
|
-
- **Runtime Decryption**: Logic that decrypts and executes memory-only payloads at runtime.
|
|
36
|
-
- **VM-based Execution**: Malicious payloads executed inside isolated virtual machine environments.
|
|
37
|
-
- **Multi-stage Loaders**: Sophisticated multi-hop obfuscation that reconstructs logic over several cycles.
|
|
38
|
-
- **Post-install Generation**: Malicious code generated or downloaded *after* the initial install/preinstall phase.
|
|
39
|
-
|
|
40
|
-
**Positioning**: Zift is a *Deterministic Pre-install Behavioral Security Gate*. It is designed to catch the most common and damaging malware patterns instantly, not to serve as a complete, multi-layer supply-chain defense.
|
|
41
|
-
|
|
42
|
-
## License
|
|
43
|
-
MIT
|
|
44
|
-
## Usage
|
|
45
|
-
|
|
46
|
-
### 🚀 Secure Installer Mode
|
|
47
|
-
Use Zift as a security gate. It will pre-audit the package source into a sandbox, show you the risk score, and ask for permission before the official installation begins.
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
# With the --zift alias (Recommended)
|
|
51
|
-
npm install axios --zift
|
|
52
|
-
|
|
53
|
-
# Directly using Zift
|
|
54
|
-
zift install gsap
|
|
55
|
-
```
|
|
21
|
+
# 1. Install Zift
|
|
22
|
+
npm install -g @7nsane/zift
|
|
56
23
|
|
|
57
|
-
|
|
58
|
-
|
|
24
|
+
# 2. Setup Secure Wrappers (adds --zift flag to npm/bun/pnpm)
|
|
25
|
+
zift setup
|
|
59
26
|
|
|
60
|
-
|
|
61
|
-
# Scan current directory
|
|
27
|
+
# 3. Audit a local project
|
|
62
28
|
zift .
|
|
63
29
|
|
|
64
|
-
#
|
|
65
|
-
zift
|
|
66
|
-
|
|
67
|
-
# CI/CD Mode (JSON output + Non-zero exit on high risk)
|
|
68
|
-
zift . --format json
|
|
30
|
+
# 4. Run your application with Active Shield
|
|
31
|
+
zift protect index.js
|
|
69
32
|
```
|
|
70
33
|
|
|
71
|
-
##
|
|
72
|
-
|
|
73
|
-
Zift uses a multi-phase engine:
|
|
74
|
-
1. **Collection**: Single-pass AST traversal to gather facts (sources, sinks, flows).
|
|
75
|
-
2. **Evaluation**: Deterministic rule matching against collected facts.
|
|
76
|
-
|
|
77
|
-
### Rule IDs:
|
|
78
|
-
- **ZFT-001 (ENV_EXFILTRATION)**: Detection of environment variables being read and sent over the network.
|
|
79
|
-
- **ZFT-002 (SENSITIVE_FILE_EXFILTRATION)**: Detection of sensitive files (e.g., `.ssh`, `.env`) being read and sent over the network.
|
|
80
|
-
- **ZFT-003 (PERSISTENCE_ATTEMPT)**: Detection of attempts to write to startup directories.
|
|
81
|
-
- **ZFT-004 (OBFUSCATED_EXECUTION)**: Detection of high-entropy strings executed via dynamic constructors.
|
|
82
|
-
|
|
83
|
-
## Key Features
|
|
84
|
-
- **Deterministic AST Analysis**: O(n) complexity, single-pass scanner.
|
|
85
|
-
- **Zero False Positives**: Verified against React, Express, and ESLint (0.0% FP rate).
|
|
86
|
-
- **Lifecycle Awareness**: Identifies if suspicious code is slated to run during `postinstall`.
|
|
87
|
-
- **Credential Protection**: Detects exfiltration of `process.env` (AWS, SSH keys, etc.) over network sinks.
|
|
88
|
-
|
|
89
|
-
## Limitations
|
|
34
|
+
## 🔍 How It Works
|
|
90
35
|
|
|
91
|
-
|
|
36
|
+
Zift uses a **Deterministic AST Analysis** engine. Unlike regex-based scanners, Zift understands the structure of your code. It tracks the flow of data from sensitive **Sources** (like `process.env`) to dangerous **Sinks** (like `fetch` or `child_process.exec`).
|
|
92
37
|
|
|
93
|
-
- **
|
|
94
|
-
- **
|
|
95
|
-
- **
|
|
38
|
+
- **Collection**: Single-pass O(n) traversal.
|
|
39
|
+
- **Evaluation**: Priority-based rule matching.
|
|
40
|
+
- **Intelligence**: Cross-file propagation and VM-based reveal.
|
|
96
41
|
|
|
97
|
-
##
|
|
42
|
+
## 🛠️ Commands
|
|
98
43
|
|
|
99
|
-
|
|
100
|
-
|
|
44
|
+
| Command | Description |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `zift .` | Deep scan of the current directory |
|
|
47
|
+
| `zift install <pkg>` | Pre-audit and install a package securely |
|
|
48
|
+
| `zift protect <app>` | Launch application with **Zift Shield** runtime auditing |
|
|
49
|
+
| `zift setup` | Configure shell aliases for secure package management |
|
|
101
50
|
|
|
102
51
|
---
|
|
103
|
-
**Build with confidence.
|
|
52
|
+
**Build with confidence. Secure with Zift.** 🛡️
|
package/bin/zift.js
CHANGED
|
@@ -23,6 +23,10 @@ async function main() {
|
|
|
23
23
|
runInit();
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
|
+
if (args[0] === 'protect') {
|
|
27
|
+
runShield(args.slice(1));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
26
30
|
|
|
27
31
|
// 2. Detection for bun/pnpm usage
|
|
28
32
|
if (args.includes('--bun')) installer = 'bun';
|
|
@@ -238,10 +242,11 @@ function runInit() {
|
|
|
238
242
|
}
|
|
239
243
|
|
|
240
244
|
function showHelp() {
|
|
241
|
-
console.log(chalk.blue.bold('\n🛡️ Zift
|
|
245
|
+
console.log(chalk.blue.bold('\n🛡️ Zift v3.0.0 - Intelligent Ecosystem Security\n'));
|
|
242
246
|
console.log('Usage:');
|
|
243
247
|
console.log(' zift setup Secure npm, bun, and pnpm');
|
|
244
248
|
console.log(' zift init Initialize configuration');
|
|
249
|
+
console.log(' zift protect <app> Run application with Zift Shield');
|
|
245
250
|
console.log(' zift install <pkg> Scan and install package');
|
|
246
251
|
console.log(' zift . Scan current directory');
|
|
247
252
|
console.log('\nOptions:');
|
|
@@ -272,4 +277,18 @@ function printSummary(findings) {
|
|
|
272
277
|
console.log(chalk.red(` Critical: ${s.Critical}\n High: ${s.High}`));
|
|
273
278
|
}
|
|
274
279
|
|
|
280
|
+
function runShield(appArgs) {
|
|
281
|
+
if (appArgs.length === 0) {
|
|
282
|
+
console.error(chalk.red('❌ Error: Specify an application to protect (e.g., zift protect main.js)'));
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const shieldPath = path.join(__dirname, '../src/shield.js');
|
|
287
|
+
const nodeArgs = ['-r', shieldPath, ...appArgs];
|
|
288
|
+
|
|
289
|
+
console.log(chalk.blue(`\n🛡️ Launching with Zift Shield...`));
|
|
290
|
+
const child = cp.spawn('node', nodeArgs, { stdio: 'inherit' });
|
|
291
|
+
child.on('exit', (code) => process.exit(code));
|
|
292
|
+
}
|
|
293
|
+
|
|
275
294
|
main();
|
package/package.json
CHANGED
package/src/collector.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const acorn = require('acorn');
|
|
2
2
|
const walk = require('acorn-walk');
|
|
3
|
+
const vm = require('node:vm');
|
|
3
4
|
const { calculateEntropy } = require('./utils/entropy');
|
|
4
5
|
|
|
5
6
|
class ASTCollector {
|
|
@@ -24,7 +25,9 @@ class ASTCollector {
|
|
|
24
25
|
SHELL_EXECUTION: [],
|
|
25
26
|
ENCODER_USE: [],
|
|
26
27
|
REMOTE_FETCH_SIGNAL: [],
|
|
27
|
-
PIPE_TO_SHELL_SIGNAL: []
|
|
28
|
+
PIPE_TO_SHELL_SIGNAL: [],
|
|
29
|
+
EXPORTS: [],
|
|
30
|
+
IMPORTS: []
|
|
28
31
|
};
|
|
29
32
|
const flows = [];
|
|
30
33
|
const sourceCode = code;
|
|
@@ -55,6 +58,57 @@ class ASTCollector {
|
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
},
|
|
61
|
+
ImportDeclaration: (node) => {
|
|
62
|
+
const source = node.source.value;
|
|
63
|
+
node.specifiers.forEach(spec => {
|
|
64
|
+
facts.IMPORTS.push({
|
|
65
|
+
file: filePath,
|
|
66
|
+
line: node.loc.start.line,
|
|
67
|
+
source,
|
|
68
|
+
local: spec.local.name,
|
|
69
|
+
imported: spec.type === 'ImportDefaultSpecifier' ? 'default' : (spec.imported ? spec.imported.name : null)
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
ExportNamedDeclaration: (node) => {
|
|
74
|
+
if (node.declaration) {
|
|
75
|
+
if (node.declaration.type === 'VariableDeclaration') {
|
|
76
|
+
node.declaration.declarations.forEach(decl => {
|
|
77
|
+
facts.EXPORTS.push({
|
|
78
|
+
file: filePath,
|
|
79
|
+
line: node.loc.start.line,
|
|
80
|
+
name: decl.id.name,
|
|
81
|
+
type: 'named'
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
} else if (node.declaration.id) {
|
|
85
|
+
facts.EXPORTS.push({
|
|
86
|
+
file: filePath,
|
|
87
|
+
line: node.loc.start.line,
|
|
88
|
+
name: node.declaration.id.name,
|
|
89
|
+
type: 'named'
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
node.specifiers.forEach(spec => {
|
|
94
|
+
facts.EXPORTS.push({
|
|
95
|
+
file: filePath,
|
|
96
|
+
line: node.loc.start.line,
|
|
97
|
+
name: spec.exported.name,
|
|
98
|
+
local: spec.local.name,
|
|
99
|
+
type: 'named'
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
ExportDefaultDeclaration: (node) => {
|
|
104
|
+
facts.EXPORTS.push({
|
|
105
|
+
file: filePath,
|
|
106
|
+
line: node.loc.start.line,
|
|
107
|
+
name: 'default',
|
|
108
|
+
local: (node.declaration.id ? node.declaration.id.name : (node.declaration.name || null)),
|
|
109
|
+
type: 'default'
|
|
110
|
+
});
|
|
111
|
+
},
|
|
58
112
|
CallExpression: (node, state, ancestors) => {
|
|
59
113
|
const calleeCode = sourceCode.substring(node.callee.start, node.callee.end);
|
|
60
114
|
|
|
@@ -66,12 +120,38 @@ class ASTCollector {
|
|
|
66
120
|
});
|
|
67
121
|
}
|
|
68
122
|
|
|
69
|
-
if (calleeCode === 'require' && node.arguments.length > 0
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
123
|
+
if (calleeCode === 'require' && node.arguments.length > 0) {
|
|
124
|
+
if (node.arguments[0].type !== 'Literal') {
|
|
125
|
+
facts.DYNAMIC_REQUIRE.push({
|
|
126
|
+
file: filePath,
|
|
127
|
+
line: node.loc.start.line,
|
|
128
|
+
variable: sourceCode.substring(node.arguments[0].start, node.arguments[0].end)
|
|
129
|
+
});
|
|
130
|
+
} else {
|
|
131
|
+
const source = node.arguments[0].value;
|
|
132
|
+
const parent = ancestors[ancestors.length - 2];
|
|
133
|
+
if (parent && parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
|
|
134
|
+
facts.IMPORTS.push({
|
|
135
|
+
file: filePath, line: node.loc.start.line, source, local: parent.id.name, imported: 'default'
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// De-obfuscation Trigger
|
|
142
|
+
const evaluated = this.tryEvaluate(node, sourceCode);
|
|
143
|
+
if (evaluated) {
|
|
144
|
+
if (this.getNetworkType(evaluated) || this.isShellSink(evaluated) || evaluated === 'eval' || evaluated === 'Function') {
|
|
145
|
+
facts.OBFUSCATION.push({
|
|
146
|
+
file: filePath,
|
|
147
|
+
line: node.loc.start.line,
|
|
148
|
+
reason: `De-obfuscated to: ${evaluated}`,
|
|
149
|
+
revealed: evaluated
|
|
150
|
+
});
|
|
151
|
+
if (evaluated === 'eval' || evaluated === 'Function') {
|
|
152
|
+
facts.DYNAMIC_EXECUTION.push({ file: filePath, line: node.loc.start.line, type: evaluated });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
75
155
|
}
|
|
76
156
|
|
|
77
157
|
const netType = this.getNetworkType(calleeCode);
|
|
@@ -90,7 +170,6 @@ class ASTCollector {
|
|
|
90
170
|
callee: calleeCode
|
|
91
171
|
});
|
|
92
172
|
|
|
93
|
-
// Signal Analysis: Dropper Patterns
|
|
94
173
|
node.arguments.forEach(arg => {
|
|
95
174
|
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
96
175
|
const val = arg.value.toLowerCase();
|
|
@@ -180,6 +259,17 @@ class ASTCollector {
|
|
|
180
259
|
}
|
|
181
260
|
},
|
|
182
261
|
AssignmentExpression: (node) => {
|
|
262
|
+
const leftCode = sourceCode.substring(node.left.start, node.left.end);
|
|
263
|
+
if (leftCode === 'module.exports' || leftCode.startsWith('exports.')) {
|
|
264
|
+
facts.EXPORTS.push({
|
|
265
|
+
file: filePath,
|
|
266
|
+
line: node.loc.start.line,
|
|
267
|
+
name: leftCode === 'module.exports' ? 'default' : leftCode.replace('exports.', ''),
|
|
268
|
+
local: (node.right.type === 'Identifier' ? node.right.name : null),
|
|
269
|
+
type: leftCode === 'module.exports' ? 'default' : 'named'
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
183
273
|
if (node.left.type === 'MemberExpression' && node.right.type === 'Identifier') {
|
|
184
274
|
const from = sourceCode.substring(node.right.start, node.right.end);
|
|
185
275
|
const to = sourceCode.substring(node.left.start, node.left.end);
|
|
@@ -216,6 +306,7 @@ class ASTCollector {
|
|
|
216
306
|
}
|
|
217
307
|
|
|
218
308
|
getNetworkType(calleeCode) {
|
|
309
|
+
if (typeof calleeCode !== 'string') return null;
|
|
219
310
|
const dnsSinks = ['dns.lookup', 'dns.resolve', 'dns.resolve4', 'dns.resolve6'];
|
|
220
311
|
const rawSocketSinks = ['net.connect', 'net.createConnection'];
|
|
221
312
|
const networkSinks = ['http.request', 'https.request', 'http.get', 'https.get', 'fetch', 'axios', 'request'];
|
|
@@ -233,6 +324,7 @@ class ASTCollector {
|
|
|
233
324
|
}
|
|
234
325
|
|
|
235
326
|
isShellSink(calleeCode) {
|
|
327
|
+
if (typeof calleeCode !== 'string') return false;
|
|
236
328
|
const shellSinks = ['child_process.exec', 'child_process.spawn', 'child_process.execSync', 'exec', 'spawn', 'execSync'];
|
|
237
329
|
return shellSinks.some(sink => {
|
|
238
330
|
if (calleeCode === sink) return true;
|
|
@@ -243,6 +335,7 @@ class ASTCollector {
|
|
|
243
335
|
}
|
|
244
336
|
|
|
245
337
|
isEncoder(calleeCode) {
|
|
338
|
+
if (typeof calleeCode !== 'string') return false;
|
|
246
339
|
const encoders = ['Buffer.from', 'btoa', 'atob', 'zlib.deflate', 'zlib.gzip', 'crypto.createCipheriv'];
|
|
247
340
|
return encoders.some(enc => calleeCode === enc || calleeCode.endsWith('.' + enc));
|
|
248
341
|
}
|
|
@@ -263,6 +356,7 @@ class ASTCollector {
|
|
|
263
356
|
}
|
|
264
357
|
|
|
265
358
|
isSensitiveFileRead(calleeCode, node, sourceCode) {
|
|
359
|
+
if (typeof calleeCode !== 'string') return false;
|
|
266
360
|
if (!calleeCode.includes('fs.readFile') && !calleeCode.includes('fs.readFileSync') &&
|
|
267
361
|
!calleeCode.includes('fs.promises.readFile')) return false;
|
|
268
362
|
|
|
@@ -275,6 +369,7 @@ class ASTCollector {
|
|
|
275
369
|
}
|
|
276
370
|
|
|
277
371
|
isStartupFileWrite(calleeCode, node, sourceCode) {
|
|
372
|
+
if (typeof calleeCode !== 'string') return false;
|
|
278
373
|
if (!calleeCode.includes('fs.writeFile') && !calleeCode.includes('fs.writeFileSync') &&
|
|
279
374
|
!calleeCode.includes('fs.appendFile')) return false;
|
|
280
375
|
|
|
@@ -286,8 +381,19 @@ class ASTCollector {
|
|
|
286
381
|
return false;
|
|
287
382
|
}
|
|
288
383
|
|
|
289
|
-
|
|
290
|
-
|
|
384
|
+
tryEvaluate(node, sourceCode) {
|
|
385
|
+
try {
|
|
386
|
+
const code = sourceCode.substring(node.start, node.end);
|
|
387
|
+
if (code.includes('process') || code.includes('require') || code.includes('fs') || code.includes('child_process')) return null;
|
|
388
|
+
if (!code.includes('[') && !code.includes('+') && !code.includes('join') && !code.includes('reverse')) return null;
|
|
389
|
+
|
|
390
|
+
const script = new vm.Script(code);
|
|
391
|
+
const context = vm.createContext({});
|
|
392
|
+
const result = script.runInContext(context, { timeout: 50 });
|
|
393
|
+
return typeof result === 'string' ? result : null;
|
|
394
|
+
} catch (e) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
291
397
|
}
|
|
292
398
|
}
|
|
293
399
|
|
package/src/scanner.js
CHANGED
|
@@ -24,6 +24,7 @@ class PackageScanner {
|
|
|
24
24
|
try { fs.mkdirSync(cacheDir, { recursive: true }); } catch (e) { }
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Initialize fact storage
|
|
27
28
|
let allFacts = {
|
|
28
29
|
facts: {
|
|
29
30
|
ENV_READ: [],
|
|
@@ -40,14 +41,16 @@ class PackageScanner {
|
|
|
40
41
|
ENCODER_USE: [],
|
|
41
42
|
REMOTE_FETCH_SIGNAL: [],
|
|
42
43
|
PIPE_TO_SHELL_SIGNAL: [],
|
|
43
|
-
LIFECYCLE_CONTEXT: []
|
|
44
|
+
LIFECYCLE_CONTEXT: [],
|
|
45
|
+
EXPORTS: [],
|
|
46
|
+
IMPORTS: []
|
|
44
47
|
},
|
|
45
48
|
flows: []
|
|
46
49
|
};
|
|
47
50
|
|
|
48
51
|
const pkgVersion = require('../package.json').version;
|
|
49
52
|
|
|
50
|
-
//
|
|
53
|
+
// Pass 1: Collection
|
|
51
54
|
const concurrency = 8;
|
|
52
55
|
for (let i = 0; i < files.length; i += concurrency) {
|
|
53
56
|
const chunk = files.slice(i, i + concurrency);
|
|
@@ -62,47 +65,84 @@ class PackageScanner {
|
|
|
62
65
|
let facts = {}, flows = [];
|
|
63
66
|
|
|
64
67
|
if (fs.existsSync(cachePath)) {
|
|
65
|
-
// Cache hit: Load metadata
|
|
66
68
|
try {
|
|
67
69
|
const cached = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
68
70
|
facts = cached.facts || {};
|
|
69
71
|
flows = cached.flows || [];
|
|
70
72
|
} catch (e) {
|
|
71
|
-
// Corrupt cache: re-scan
|
|
72
73
|
const result = this.collector.collect(code, file);
|
|
73
74
|
facts = result.facts;
|
|
74
75
|
flows = result.flows;
|
|
75
76
|
}
|
|
76
77
|
} else {
|
|
77
|
-
// Cache miss: Scan and save
|
|
78
78
|
const result = this.collector.collect(code, file);
|
|
79
79
|
facts = result.facts;
|
|
80
80
|
flows = result.flows;
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
fs.writeFileSync(cachePath, JSON.stringify({ facts, flows }));
|
|
84
|
-
} catch (e) { }
|
|
81
|
+
try { fs.writeFileSync(cachePath, JSON.stringify({ facts, flows })); } catch (e) { }
|
|
85
82
|
}
|
|
86
83
|
|
|
87
|
-
// Merge facts (Synchronized)
|
|
88
84
|
if (lifecycleFiles.has(file)) {
|
|
89
85
|
facts.LIFECYCLE_CONTEXT = facts.LIFECYCLE_CONTEXT || [];
|
|
90
86
|
facts.LIFECYCLE_CONTEXT.push({ file, reason: 'Lifecycle script context detected' });
|
|
91
87
|
}
|
|
92
88
|
|
|
93
89
|
for (const category in facts) {
|
|
94
|
-
if (allFacts.facts[category])
|
|
95
|
-
allFacts.facts[category].push(...facts[category]);
|
|
96
|
-
}
|
|
90
|
+
if (allFacts.facts[category]) allFacts.facts[category].push(...facts[category]);
|
|
97
91
|
}
|
|
98
92
|
allFacts.flows.push(...flows);
|
|
99
93
|
}));
|
|
100
94
|
}
|
|
101
95
|
|
|
96
|
+
// Pass 2: Cross-File Taint Resolution
|
|
97
|
+
this.resolveCrossFileTaint(allFacts);
|
|
98
|
+
|
|
102
99
|
const findings = this.engine.evaluate(allFacts, lifecycleFiles);
|
|
103
100
|
return this.formatFindings(findings);
|
|
104
101
|
}
|
|
105
102
|
|
|
103
|
+
resolveCrossFileTaint(allFacts) {
|
|
104
|
+
const { facts, flows } = allFacts;
|
|
105
|
+
const exportMap = new Map(); // file -> exportName -> localName/isTainted
|
|
106
|
+
|
|
107
|
+
// 1. Build Export Map
|
|
108
|
+
facts.EXPORTS.forEach(exp => {
|
|
109
|
+
if (!exportMap.has(exp.file)) exportMap.set(exp.file, new Map());
|
|
110
|
+
|
|
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'));
|
|
114
|
+
|
|
115
|
+
exportMap.get(exp.file).set(exp.name, {
|
|
116
|
+
local: exp.local,
|
|
117
|
+
isTainted: isLocalTainted || isNamedTainted
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// 2. Propagate to Imports
|
|
122
|
+
facts.IMPORTS.forEach(imp => {
|
|
123
|
+
let resolvedPath;
|
|
124
|
+
if (imp.source.startsWith('.')) {
|
|
125
|
+
resolvedPath = path.resolve(path.dirname(imp.file), imp.source);
|
|
126
|
+
if (!resolvedPath.endsWith('.js')) resolvedPath += '.js';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (resolvedPath && exportMap.has(resolvedPath)) {
|
|
130
|
+
const targetExports = exportMap.get(resolvedPath);
|
|
131
|
+
const matchedExport = targetExports.get(imp.imported);
|
|
132
|
+
|
|
133
|
+
if (matchedExport && matchedExport.isTainted) {
|
|
134
|
+
// Mark as a virtual ENV_READ in the importing file
|
|
135
|
+
facts.ENV_READ.push({
|
|
136
|
+
file: imp.file,
|
|
137
|
+
line: imp.line,
|
|
138
|
+
variable: `[Cross-File] ${imp.local} (from ${imp.source})`,
|
|
139
|
+
isCrossFile: true
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
106
146
|
async getFiles() {
|
|
107
147
|
// Load .ziftignore
|
|
108
148
|
const ziftIgnorePath = path.join(this.packageDir, '.ziftignore');
|
package/src/shield.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const diagnostics = require('node:diagnostics_channel');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zift Shield Runtime Guard
|
|
5
|
+
* Intercepts network and shell activity at runtime for security auditing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function setupShield() {
|
|
9
|
+
console.warn('🛡️ ZIFT SHIELD ACTIVE: Monitoring suspicious runtime activity...');
|
|
10
|
+
|
|
11
|
+
// 1. Monitor Network Activity via diagnostics_channel
|
|
12
|
+
const netChannel = diagnostics.channel('net.client.socket.request.start');
|
|
13
|
+
netChannel.subscribe(({ address, port }) => {
|
|
14
|
+
console.warn(`[ZIFT-SHIELD] 🌐 Outbound Connection: ${address}:${port}`);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// 2. Wrap Child Process (for shell command execution)
|
|
18
|
+
const cp = require('node:child_process');
|
|
19
|
+
['exec', 'spawn', 'execSync', 'spawnSync'].forEach(method => {
|
|
20
|
+
const original = cp[method];
|
|
21
|
+
cp[method] = function (...args) {
|
|
22
|
+
const command = args[0];
|
|
23
|
+
const cmdStr = typeof command === 'string' ? command : (args[1] ? args[1].join(' ') : 'unknown');
|
|
24
|
+
console.warn(`[ZIFT-SHIELD] 🐚 Shell Execution: ${cmdStr}`);
|
|
25
|
+
|
|
26
|
+
// Heuristic Check: Is it a potential dropper?
|
|
27
|
+
if (cmdStr.includes('curl') || cmdStr.includes('wget') || cmdStr.includes('| sh')) {
|
|
28
|
+
console.error(`[ZIFT-SHIELD] ⚠️ CRITICAL: Potential Remote Dropper detected in shell execution!`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return original.apply(this, args);
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// 3. Monitor HTTP/HTTPS
|
|
36
|
+
const http = require('node:http');
|
|
37
|
+
const https = require('node:https');
|
|
38
|
+
[http, https].forEach(mod => {
|
|
39
|
+
['request', 'get'].forEach(method => {
|
|
40
|
+
const original = mod[method];
|
|
41
|
+
mod[method] = function (...args) {
|
|
42
|
+
let url = args[0];
|
|
43
|
+
if (typeof url === 'object' && url.href) url = url.href;
|
|
44
|
+
else if (typeof url === 'string') url = url;
|
|
45
|
+
else url = `${args[0].host || args[0].hostname}${args[0].path || ''}`;
|
|
46
|
+
|
|
47
|
+
console.warn(`[ZIFT-SHIELD] 📡 HTTP Request: ${url}`);
|
|
48
|
+
return original.apply(this, args);
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Auto-activate if required via node -r
|
|
55
|
+
setupShield();
|
|
56
|
+
|
|
57
|
+
module.exports = { setupShield };
|