@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 CHANGED
@@ -1,103 +1,53 @@
1
- # ๐Ÿ›ก๏ธ Zift (v2.0.0)
1
+ # ๐Ÿ›ก๏ธ Zift (v4.0.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 Deterministic Pre-install Security Gate for JavaScript Projects.** By using deterministic AST analysis and lightweight variable propagation, Zift identifies potential credential exfiltration, malicious persistence, and obfuscated execution with extreme precision.
7
+ **The Deeply Hardened Ecosystem Security Engine for JavaScript.**
8
8
 
9
- ## Installation
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
- ```bash
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
- ## ๐Ÿ›ก๏ธ Secure Your Workflow (Recommended)
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
- Set up the **Secure npm Wrapper** to audit packages automatically every time you install something.
19
+ ## ๐Ÿ“ฆ Quick Start
19
20
 
20
21
  ```bash
21
- # 1. Run the setup
22
- zift setup
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
- ### ๐Ÿ” Advanced Scanning
58
- Scan local directories or existing dependencies in your `node_modules`.
25
+ # 2. Setup Secure Wrappers (adds --zift flag to npm/bun/pnpm)
26
+ zift setup
59
27
 
60
- ```bash
61
- # Scan current directory
28
+ # 3. Audit a local project
62
29
  zift .
63
30
 
64
- # Scan a specific folder or dependency
65
- zift ./node_modules/example-pkg
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
- ## Rule Transparency
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
- Transparency is key to trust. As a V1 static analysis tool, Zift has the following scope boundaries:
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
- - **No Interprocedural Flow**: Variable tracking is restricted to function scope; it does not track data across function boundaries.
94
- - **No Cross-File Propagation**: Analysis is performed on a per-file basis.
95
- - **No Dynamic Runtime Analysis**: Zift does not execute code; it cannot detect evasion techniques that only trigger during execution.
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
- ## Performance Guarantees
43
+ ## ๐Ÿ› ๏ธ Commands
98
44
 
99
- - **File Cap**: Files larger than **512KB** are skipped to ensure predictable scan times.
100
- - **String Cap**: Entropy calculation is skipped for literal strings longer than **2048 characters**.
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. Scan with Zift.** ๐Ÿ›ก๏ธ
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 v2.0.0 - Intelligent Pre-install Security Gate\n'));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7nsane/zift",
3
- "version": "2.2.1",
3
+ "version": "4.0.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
@@ -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 && 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
+ }
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 && node.arguments[0].type !== 'Literal') {
70
- facts.DYNAMIC_REQUIRE.push({
71
- file: filePath,
72
- line: node.loc.start.line,
73
- variable: sourceCode.substring(node.arguments[0].start, node.arguments[0].end)
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
- getSourceCode(node) {
290
- return this.sourceCode.substring(node.start, node.end);
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
 
@@ -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
- // Parallel processing with limited concurrency (8 files at a time)
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 };