@7nsane/zift 2.2.1 โ 4.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 +30 -80
- package/bin/zift.js +20 -1
- package/package.json +1 -1
- package/src/collector.js +144 -11
- package/src/rules/definitions.js +28 -1
- package/src/scanner.js +63 -14
- package/src/shield.js +98 -0
package/README.md
CHANGED
|
@@ -1,103 +1,53 @@
|
|
|
1
|
-
# ๐ก๏ธ Zift (
|
|
1
|
+
# ๐ก๏ธ Zift (v4.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 Deeply Hardened Ecosystem Security Engine for JavaScript.**
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Zift v4.0 is the "Deep Hardening" release, featuring **Immutable Runtime Guards** and **Opaque Payload Detection**, specifically designed to resist active attacker bypasses.
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
# Install globally to use the 'zift' command anywhere
|
|
13
|
-
npm install -g @7nsane/zift
|
|
14
|
-
```
|
|
11
|
+
## ๐ Key Advancements (v4.0.0)
|
|
15
12
|
|
|
16
|
-
|
|
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.
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
## ๐ฆ Quick Start
|
|
19
20
|
|
|
20
21
|
```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
|
-
```
|
|
22
|
+
# 1. Install Zift
|
|
23
|
+
npm install -g @7nsane/zift
|
|
56
24
|
|
|
57
|
-
|
|
58
|
-
|
|
25
|
+
# 2. Setup Secure Wrappers (adds --zift flag to npm/bun/pnpm)
|
|
26
|
+
zift setup
|
|
59
27
|
|
|
60
|
-
|
|
61
|
-
# Scan current directory
|
|
28
|
+
# 3. Audit a local project
|
|
62
29
|
zift .
|
|
63
30
|
|
|
64
|
-
#
|
|
65
|
-
zift
|
|
66
|
-
|
|
67
|
-
# CI/CD Mode (JSON output + Non-zero exit on high risk)
|
|
68
|
-
zift . --format json
|
|
31
|
+
# 4. Run your application with Active Shield
|
|
32
|
+
zift protect index.js
|
|
69
33
|
```
|
|
70
34
|
|
|
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
|
|
35
|
+
## ๐ How It Works
|
|
90
36
|
|
|
91
|
-
|
|
37
|
+
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
38
|
|
|
93
|
-
- **
|
|
94
|
-
- **
|
|
95
|
-
- **
|
|
39
|
+
- **Collection**: Single-pass O(n) traversal.
|
|
40
|
+
- **Evaluation**: Priority-based rule matching.
|
|
41
|
+
- **Intelligence**: Cross-file propagation and VM-based reveal.
|
|
96
42
|
|
|
97
|
-
##
|
|
43
|
+
## ๐ ๏ธ Commands
|
|
98
44
|
|
|
99
|
-
|
|
100
|
-
|
|
45
|
+
| Command | Description |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `zift .` | Deep scan of the current directory |
|
|
48
|
+
| `zift install <pkg>` | Pre-audit and install a package securely |
|
|
49
|
+
| `zift protect <app>` | Launch application with **Zift Shield** runtime auditing |
|
|
50
|
+
| `zift setup` | Configure shell aliases for secure package management |
|
|
101
51
|
|
|
102
52
|
---
|
|
103
|
-
**Build with confidence.
|
|
53
|
+
**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,11 @@ 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: [],
|
|
31
|
+
OPAQUE_STRING_SKIP: [],
|
|
32
|
+
NON_DETERMINISTIC_SINK: []
|
|
28
33
|
};
|
|
29
34
|
const flows = [];
|
|
30
35
|
const sourceCode = code;
|
|
@@ -43,7 +48,20 @@ class ASTCollector {
|
|
|
43
48
|
|
|
44
49
|
walk.ancestor(ast, {
|
|
45
50
|
Literal: (node) => {
|
|
46
|
-
if (typeof node.value === 'string' && node.value.length > 20
|
|
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
|
+
}
|
|
47
65
|
const entropy = calculateEntropy(node.value);
|
|
48
66
|
if (entropy > this.entropyThreshold) {
|
|
49
67
|
facts.OBFUSCATION.push({
|
|
@@ -55,6 +73,57 @@ class ASTCollector {
|
|
|
55
73
|
}
|
|
56
74
|
}
|
|
57
75
|
},
|
|
76
|
+
ImportDeclaration: (node) => {
|
|
77
|
+
const source = node.source.value;
|
|
78
|
+
node.specifiers.forEach(spec => {
|
|
79
|
+
facts.IMPORTS.push({
|
|
80
|
+
file: filePath,
|
|
81
|
+
line: node.loc.start.line,
|
|
82
|
+
source,
|
|
83
|
+
local: spec.local.name,
|
|
84
|
+
imported: spec.type === 'ImportDefaultSpecifier' ? 'default' : (spec.imported ? spec.imported.name : null)
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
ExportNamedDeclaration: (node) => {
|
|
89
|
+
if (node.declaration) {
|
|
90
|
+
if (node.declaration.type === 'VariableDeclaration') {
|
|
91
|
+
node.declaration.declarations.forEach(decl => {
|
|
92
|
+
facts.EXPORTS.push({
|
|
93
|
+
file: filePath,
|
|
94
|
+
line: node.loc.start.line,
|
|
95
|
+
name: decl.id.name,
|
|
96
|
+
type: 'named'
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
} else if (node.declaration.id) {
|
|
100
|
+
facts.EXPORTS.push({
|
|
101
|
+
file: filePath,
|
|
102
|
+
line: node.loc.start.line,
|
|
103
|
+
name: node.declaration.id.name,
|
|
104
|
+
type: 'named'
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
node.specifiers.forEach(spec => {
|
|
109
|
+
facts.EXPORTS.push({
|
|
110
|
+
file: filePath,
|
|
111
|
+
line: node.loc.start.line,
|
|
112
|
+
name: spec.exported.name,
|
|
113
|
+
local: spec.local.name,
|
|
114
|
+
type: 'named'
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
ExportDefaultDeclaration: (node) => {
|
|
119
|
+
facts.EXPORTS.push({
|
|
120
|
+
file: filePath,
|
|
121
|
+
line: node.loc.start.line,
|
|
122
|
+
name: 'default',
|
|
123
|
+
local: (node.declaration.id ? node.declaration.id.name : (node.declaration.name || null)),
|
|
124
|
+
type: 'default'
|
|
125
|
+
});
|
|
126
|
+
},
|
|
58
127
|
CallExpression: (node, state, ancestors) => {
|
|
59
128
|
const calleeCode = sourceCode.substring(node.callee.start, node.callee.end);
|
|
60
129
|
|
|
@@ -66,12 +135,38 @@ class ASTCollector {
|
|
|
66
135
|
});
|
|
67
136
|
}
|
|
68
137
|
|
|
69
|
-
if (calleeCode === 'require' && node.arguments.length > 0
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
138
|
+
if (calleeCode === 'require' && node.arguments.length > 0) {
|
|
139
|
+
if (node.arguments[0].type !== 'Literal') {
|
|
140
|
+
facts.DYNAMIC_REQUIRE.push({
|
|
141
|
+
file: filePath,
|
|
142
|
+
line: node.loc.start.line,
|
|
143
|
+
variable: sourceCode.substring(node.arguments[0].start, node.arguments[0].end)
|
|
144
|
+
});
|
|
145
|
+
} else {
|
|
146
|
+
const source = node.arguments[0].value;
|
|
147
|
+
const parent = ancestors[ancestors.length - 2];
|
|
148
|
+
if (parent && parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
|
|
149
|
+
facts.IMPORTS.push({
|
|
150
|
+
file: filePath, line: node.loc.start.line, source, local: parent.id.name, imported: 'default'
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// De-obfuscation Trigger
|
|
157
|
+
const evaluated = this.tryEvaluate(node, sourceCode);
|
|
158
|
+
if (evaluated) {
|
|
159
|
+
if (this.getNetworkType(evaluated) || this.isShellSink(evaluated) || evaluated === 'eval' || evaluated === 'Function') {
|
|
160
|
+
facts.OBFUSCATION.push({
|
|
161
|
+
file: filePath,
|
|
162
|
+
line: node.loc.start.line,
|
|
163
|
+
reason: `De-obfuscated to: ${evaluated}`,
|
|
164
|
+
revealed: evaluated
|
|
165
|
+
});
|
|
166
|
+
if (evaluated === 'eval' || evaluated === 'Function') {
|
|
167
|
+
facts.DYNAMIC_EXECUTION.push({ file: filePath, line: node.loc.start.line, type: evaluated });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
75
170
|
}
|
|
76
171
|
|
|
77
172
|
const netType = this.getNetworkType(calleeCode);
|
|
@@ -90,7 +185,6 @@ class ASTCollector {
|
|
|
90
185
|
callee: calleeCode
|
|
91
186
|
});
|
|
92
187
|
|
|
93
|
-
// Signal Analysis: Dropper Patterns
|
|
94
188
|
node.arguments.forEach(arg => {
|
|
95
189
|
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
96
190
|
const val = arg.value.toLowerCase();
|
|
@@ -147,6 +241,18 @@ class ASTCollector {
|
|
|
147
241
|
});
|
|
148
242
|
}
|
|
149
243
|
}
|
|
244
|
+
|
|
245
|
+
// v4.0 Hardening: Non-deterministic constructor
|
|
246
|
+
if (['Math.random', 'Date.now', 'Date()'].some(t => argCode.includes(t))) {
|
|
247
|
+
if (evaluated === 'eval' || evaluated === 'Function' || this.isShellSink(calleeCode)) {
|
|
248
|
+
facts.NON_DETERMINISTIC_SINK.push({
|
|
249
|
+
file: filePath,
|
|
250
|
+
line: node.loc.start.line,
|
|
251
|
+
callee: calleeCode,
|
|
252
|
+
reason: `Sink uses non-deterministic argument (${argCode})`
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
150
256
|
});
|
|
151
257
|
},
|
|
152
258
|
MemberExpression: (node) => {
|
|
@@ -180,6 +286,17 @@ class ASTCollector {
|
|
|
180
286
|
}
|
|
181
287
|
},
|
|
182
288
|
AssignmentExpression: (node) => {
|
|
289
|
+
const leftCode = sourceCode.substring(node.left.start, node.left.end);
|
|
290
|
+
if (leftCode === 'module.exports' || leftCode.startsWith('exports.')) {
|
|
291
|
+
facts.EXPORTS.push({
|
|
292
|
+
file: filePath,
|
|
293
|
+
line: node.loc.start.line,
|
|
294
|
+
name: leftCode === 'module.exports' ? 'default' : leftCode.replace('exports.', ''),
|
|
295
|
+
local: (node.right.type === 'Identifier' ? node.right.name : null),
|
|
296
|
+
type: leftCode === 'module.exports' ? 'default' : 'named'
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
183
300
|
if (node.left.type === 'MemberExpression' && node.right.type === 'Identifier') {
|
|
184
301
|
const from = sourceCode.substring(node.right.start, node.right.end);
|
|
185
302
|
const to = sourceCode.substring(node.left.start, node.left.end);
|
|
@@ -216,6 +333,7 @@ class ASTCollector {
|
|
|
216
333
|
}
|
|
217
334
|
|
|
218
335
|
getNetworkType(calleeCode) {
|
|
336
|
+
if (typeof calleeCode !== 'string') return null;
|
|
219
337
|
const dnsSinks = ['dns.lookup', 'dns.resolve', 'dns.resolve4', 'dns.resolve6'];
|
|
220
338
|
const rawSocketSinks = ['net.connect', 'net.createConnection'];
|
|
221
339
|
const networkSinks = ['http.request', 'https.request', 'http.get', 'https.get', 'fetch', 'axios', 'request'];
|
|
@@ -233,6 +351,7 @@ class ASTCollector {
|
|
|
233
351
|
}
|
|
234
352
|
|
|
235
353
|
isShellSink(calleeCode) {
|
|
354
|
+
if (typeof calleeCode !== 'string') return false;
|
|
236
355
|
const shellSinks = ['child_process.exec', 'child_process.spawn', 'child_process.execSync', 'exec', 'spawn', 'execSync'];
|
|
237
356
|
return shellSinks.some(sink => {
|
|
238
357
|
if (calleeCode === sink) return true;
|
|
@@ -243,6 +362,7 @@ class ASTCollector {
|
|
|
243
362
|
}
|
|
244
363
|
|
|
245
364
|
isEncoder(calleeCode) {
|
|
365
|
+
if (typeof calleeCode !== 'string') return false;
|
|
246
366
|
const encoders = ['Buffer.from', 'btoa', 'atob', 'zlib.deflate', 'zlib.gzip', 'crypto.createCipheriv'];
|
|
247
367
|
return encoders.some(enc => calleeCode === enc || calleeCode.endsWith('.' + enc));
|
|
248
368
|
}
|
|
@@ -263,6 +383,7 @@ class ASTCollector {
|
|
|
263
383
|
}
|
|
264
384
|
|
|
265
385
|
isSensitiveFileRead(calleeCode, node, sourceCode) {
|
|
386
|
+
if (typeof calleeCode !== 'string') return false;
|
|
266
387
|
if (!calleeCode.includes('fs.readFile') && !calleeCode.includes('fs.readFileSync') &&
|
|
267
388
|
!calleeCode.includes('fs.promises.readFile')) return false;
|
|
268
389
|
|
|
@@ -275,6 +396,7 @@ class ASTCollector {
|
|
|
275
396
|
}
|
|
276
397
|
|
|
277
398
|
isStartupFileWrite(calleeCode, node, sourceCode) {
|
|
399
|
+
if (typeof calleeCode !== 'string') return false;
|
|
278
400
|
if (!calleeCode.includes('fs.writeFile') && !calleeCode.includes('fs.writeFileSync') &&
|
|
279
401
|
!calleeCode.includes('fs.appendFile')) return false;
|
|
280
402
|
|
|
@@ -286,8 +408,19 @@ class ASTCollector {
|
|
|
286
408
|
return false;
|
|
287
409
|
}
|
|
288
410
|
|
|
289
|
-
|
|
290
|
-
|
|
411
|
+
tryEvaluate(node, sourceCode) {
|
|
412
|
+
try {
|
|
413
|
+
const code = sourceCode.substring(node.start, node.end);
|
|
414
|
+
if (code.includes('process') || code.includes('require') || code.includes('fs') || code.includes('child_process')) return null;
|
|
415
|
+
if (!code.includes('[') && !code.includes('+') && !code.includes('join') && !code.includes('reverse')) return null;
|
|
416
|
+
|
|
417
|
+
const script = new vm.Script(code);
|
|
418
|
+
const context = vm.createContext({});
|
|
419
|
+
const result = script.runInContext(context, { timeout: 50 });
|
|
420
|
+
return typeof result === 'string' ? result : null;
|
|
421
|
+
} catch (e) {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
291
424
|
}
|
|
292
425
|
}
|
|
293
426
|
|
package/src/rules/definitions.js
CHANGED
|
@@ -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
|
@@ -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,18 +41,29 @@ 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: [],
|
|
47
|
+
NATIVE_BINARY_DETECTED: [],
|
|
48
|
+
OPAQUE_STRING_SKIP: []
|
|
44
49
|
},
|
|
45
50
|
flows: []
|
|
46
51
|
};
|
|
47
52
|
|
|
48
53
|
const pkgVersion = require('../package.json').version;
|
|
49
54
|
|
|
50
|
-
//
|
|
55
|
+
// Pass 1: Collection
|
|
51
56
|
const concurrency = 8;
|
|
52
57
|
for (let i = 0; i < files.length; i += concurrency) {
|
|
53
58
|
const chunk = files.slice(i, i + concurrency);
|
|
54
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
|
+
}
|
|
55
67
|
const stats = fs.statSync(file);
|
|
56
68
|
if (stats.size > 512 * 1024) return;
|
|
57
69
|
|
|
@@ -62,47 +74,84 @@ class PackageScanner {
|
|
|
62
74
|
let facts = {}, flows = [];
|
|
63
75
|
|
|
64
76
|
if (fs.existsSync(cachePath)) {
|
|
65
|
-
// Cache hit: Load metadata
|
|
66
77
|
try {
|
|
67
78
|
const cached = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
68
79
|
facts = cached.facts || {};
|
|
69
80
|
flows = cached.flows || [];
|
|
70
81
|
} catch (e) {
|
|
71
|
-
// Corrupt cache: re-scan
|
|
72
82
|
const result = this.collector.collect(code, file);
|
|
73
83
|
facts = result.facts;
|
|
74
84
|
flows = result.flows;
|
|
75
85
|
}
|
|
76
86
|
} else {
|
|
77
|
-
// Cache miss: Scan and save
|
|
78
87
|
const result = this.collector.collect(code, file);
|
|
79
88
|
facts = result.facts;
|
|
80
89
|
flows = result.flows;
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
fs.writeFileSync(cachePath, JSON.stringify({ facts, flows }));
|
|
84
|
-
} catch (e) { }
|
|
90
|
+
try { fs.writeFileSync(cachePath, JSON.stringify({ facts, flows })); } catch (e) { }
|
|
85
91
|
}
|
|
86
92
|
|
|
87
|
-
// Merge facts (Synchronized)
|
|
88
93
|
if (lifecycleFiles.has(file)) {
|
|
89
94
|
facts.LIFECYCLE_CONTEXT = facts.LIFECYCLE_CONTEXT || [];
|
|
90
95
|
facts.LIFECYCLE_CONTEXT.push({ file, reason: 'Lifecycle script context detected' });
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
for (const category in facts) {
|
|
94
|
-
if (allFacts.facts[category])
|
|
95
|
-
allFacts.facts[category].push(...facts[category]);
|
|
96
|
-
}
|
|
99
|
+
if (allFacts.facts[category]) allFacts.facts[category].push(...facts[category]);
|
|
97
100
|
}
|
|
98
101
|
allFacts.flows.push(...flows);
|
|
99
102
|
}));
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
// Pass 2: Cross-File Taint Resolution
|
|
106
|
+
this.resolveCrossFileTaint(allFacts);
|
|
107
|
+
|
|
102
108
|
const findings = this.engine.evaluate(allFacts, lifecycleFiles);
|
|
103
109
|
return this.formatFindings(findings);
|
|
104
110
|
}
|
|
105
111
|
|
|
112
|
+
resolveCrossFileTaint(allFacts) {
|
|
113
|
+
const { facts, flows } = allFacts;
|
|
114
|
+
const exportMap = new Map(); // file -> exportName -> localName/isTainted
|
|
115
|
+
|
|
116
|
+
// 1. Build Export Map
|
|
117
|
+
facts.EXPORTS.forEach(exp => {
|
|
118
|
+
if (!exportMap.has(exp.file)) exportMap.set(exp.file, new Map());
|
|
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'));
|
|
123
|
+
|
|
124
|
+
exportMap.get(exp.file).set(exp.name, {
|
|
125
|
+
local: exp.local,
|
|
126
|
+
isTainted: isLocalTainted || isNamedTainted
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// 2. Propagate to Imports
|
|
131
|
+
facts.IMPORTS.forEach(imp => {
|
|
132
|
+
let resolvedPath;
|
|
133
|
+
if (imp.source.startsWith('.')) {
|
|
134
|
+
resolvedPath = path.resolve(path.dirname(imp.file), imp.source);
|
|
135
|
+
if (!resolvedPath.endsWith('.js')) resolvedPath += '.js';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (resolvedPath && exportMap.has(resolvedPath)) {
|
|
139
|
+
const targetExports = exportMap.get(resolvedPath);
|
|
140
|
+
const matchedExport = targetExports.get(imp.imported);
|
|
141
|
+
|
|
142
|
+
if (matchedExport && matchedExport.isTainted) {
|
|
143
|
+
// Mark as a virtual ENV_READ in the importing file
|
|
144
|
+
facts.ENV_READ.push({
|
|
145
|
+
file: imp.file,
|
|
146
|
+
line: imp.line,
|
|
147
|
+
variable: `[Cross-File] ${imp.local} (from ${imp.source})`,
|
|
148
|
+
isCrossFile: true
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
106
155
|
async getFiles() {
|
|
107
156
|
// Load .ziftignore
|
|
108
157
|
const ziftIgnorePath = path.join(this.packageDir, '.ziftignore');
|
|
@@ -126,7 +175,7 @@ class PackageScanner {
|
|
|
126
175
|
const stat = fs.statSync(fullPath);
|
|
127
176
|
if (stat && stat.isDirectory()) {
|
|
128
177
|
results.push(...getJsFiles(fullPath));
|
|
129
|
-
} else if (file.endsWith('.js')) {
|
|
178
|
+
} else if (file.endsWith('.js') || file.endsWith('.node')) {
|
|
130
179
|
results.push(fullPath);
|
|
131
180
|
}
|
|
132
181
|
}
|
package/src/shield.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
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) - IMMUTABLE
|
|
18
|
+
const cp = require('node:child_process');
|
|
19
|
+
['exec', 'spawn', 'execSync', 'spawnSync'].forEach(method => {
|
|
20
|
+
const original = cp[method];
|
|
21
|
+
if (!original) return;
|
|
22
|
+
|
|
23
|
+
const wrapper = function (...args) {
|
|
24
|
+
const command = args[0];
|
|
25
|
+
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
|
+
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!`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return original.apply(this, args);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
Object.defineProperty(cp, method, { value: wrapper, writable: false, configurable: false });
|
|
37
|
+
} catch (e) {
|
|
38
|
+
cp[method] = wrapper; // Fallback
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// 3. Monitor HTTP/HTTPS - IMMUTABLE
|
|
43
|
+
const http = require('node:http');
|
|
44
|
+
const https = require('node:https');
|
|
45
|
+
[http, https].forEach(mod => {
|
|
46
|
+
['request', 'get'].forEach(method => {
|
|
47
|
+
const original = mod[method];
|
|
48
|
+
const wrapper = function (...args) {
|
|
49
|
+
let url = args[0];
|
|
50
|
+
if (typeof url === 'object' && url.href) url = url.href;
|
|
51
|
+
else if (typeof url === 'string') url = url;
|
|
52
|
+
else url = `${args[0].host || args[0].hostname}${args[0].path || ''}`;
|
|
53
|
+
|
|
54
|
+
console.warn(`[ZIFT-SHIELD] ๐ก HTTP Request: ${url}`);
|
|
55
|
+
return original.apply(this, args);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
Object.defineProperty(mod, method, { value: wrapper, writable: false, configurable: false });
|
|
60
|
+
} catch (e) {
|
|
61
|
+
mod[method] = wrapper;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
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) { }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Auto-activate if required via node -r
|
|
96
|
+
setupShield();
|
|
97
|
+
|
|
98
|
+
module.exports = { setupShield };
|