@7nsane/zift 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # 🛡️ Zift (v2.0.0)
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@7nsane/zift.svg?style=flat-square)](https://www.npmjs.com/package/@7nsane/zift)
4
+ [![License](https://img.shields.io/npm/l/@7nsane/zift.svg?style=flat-square)](https://www.npmjs.com/package/@7nsane/zift)
5
+ [![Build Status](https://img.shields.io/badge/CI-passing-brightgreen?style=flat-square)](https://github.com/7nsane/zift)
6
+
3
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.
4
8
 
5
9
  ## Installation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7nsane/zift",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "A high-performance, deterministic security scanner for npm packages.",
5
5
  "main": "src/scanner.js",
6
6
  "bin": {
package/src/collector.js CHANGED
@@ -100,6 +100,14 @@ class ASTCollector {
100
100
  });
101
101
  }
102
102
 
103
+ if (this.isStartupFileWrite(calleeCode, node, sourceCode)) {
104
+ facts.FILE_WRITE_STARTUP.push({
105
+ file: filePath,
106
+ line: node.loc.start.line,
107
+ path: node.arguments[0] ? sourceCode.substring(node.arguments[0].start, node.arguments[0].end) : 'unknown'
108
+ });
109
+ }
110
+
103
111
  node.arguments.forEach((arg, index) => {
104
112
  const argCode = sourceCode.substring(arg.start, arg.end);
105
113
  // Improved check: Does the expression contain any variable we know is tainted?
@@ -186,7 +194,7 @@ class ASTCollector {
186
194
  isNetworkSink(calleeCode) {
187
195
  const methodSinks = [
188
196
  'http.request', 'https.request', 'http.get', 'https.get',
189
- 'net.connect', 'dns.lookup', 'dns.resolve', 'dns.resolve4', 'dns.resolve6',
197
+ 'net.connect', 'net.createConnection', 'dns.lookup', 'dns.resolve', 'dns.resolve4', 'dns.resolve6',
190
198
  'fetch', 'axios', 'request'
191
199
  ];
192
200
  // Improved matching for require('https').get patterns
@@ -210,7 +218,7 @@ class ASTCollector {
210
218
  }
211
219
 
212
220
  isEncoder(calleeCode) {
213
- const encoders = ['Buffer.from', 'btoa', 'atob'];
221
+ const encoders = ['Buffer.from', 'btoa', 'atob', 'zlib.deflate', 'zlib.gzip', 'crypto.createCipheriv'];
214
222
  return encoders.some(enc => calleeCode === enc || calleeCode.endsWith('.' + enc));
215
223
  }
216
224
 
@@ -235,12 +243,24 @@ class ASTCollector {
235
243
 
236
244
  if (node.arguments.length > 0 && node.arguments[0].type === 'Literal') {
237
245
  const pathValue = String(node.arguments[0].value);
238
- const sensitive = ['.ssh', '.env', 'shadow', 'passwd', 'credentials', 'token'];
246
+ const sensitive = ['.ssh', '.env', 'shadow', 'passwd', 'credentials', 'token', '_netrc', 'aws_access_key'];
239
247
  return sensitive.some((s) => pathValue.toLowerCase().includes(s));
240
248
  }
241
249
  return false;
242
250
  }
243
251
 
252
+ isStartupFileWrite(calleeCode, node, sourceCode) {
253
+ if (!calleeCode.includes('fs.writeFile') && !calleeCode.includes('fs.writeFileSync') &&
254
+ !calleeCode.includes('fs.appendFile')) return false;
255
+
256
+ if (node.arguments.length > 0 && node.arguments[0].type === 'Literal') {
257
+ const pathValue = String(node.arguments[0].value);
258
+ const startup = ['package.json', '.npmrc', '.bashrc', '.zshrc', 'crontab', 'init.d', 'systemd'];
259
+ return startup.some((s) => pathValue.toLowerCase().includes(s));
260
+ }
261
+ return false;
262
+ }
263
+
244
264
  getSourceCode(node) {
245
265
  return this.sourceCode.substring(node.start, node.end);
246
266
  }
package/src/engine.js CHANGED
@@ -37,6 +37,26 @@ class SafetyEngine {
37
37
  }
38
38
 
39
39
  if (matchedFacts.length === 0) return null; // Rule not matched
40
+
41
+ // Specialist Rule: DNS Exfiltration (ZFT-007) requires a DNS-specific sink
42
+ if (rule.alias === 'DNS_EXFILTRATION' && req === 'NETWORK_SINK') {
43
+ matchedFacts = matchedFacts.filter(f => f.callee && f.callee.includes('dns'));
44
+ if (matchedFacts.length === 0) return null;
45
+ }
46
+
47
+ // Specialist Rule: Raw Socket Tunnel (ZFT-011) requires net.connect or similar
48
+ if (rule.alias === 'RAW_SOCKET_TUNNEL' && req === 'NETWORK_SINK') {
49
+ matchedFacts = matchedFacts.filter(f => f.callee && (f.callee.includes('net.connect') || f.callee.includes('net.createConnection')));
50
+ if (matchedFacts.length === 0) return null;
51
+ }
52
+
53
+ // Specialist Rule: Startup Mod (ZFT-012) requires specific file paths
54
+ if (rule.alias === 'STARTUP_SCRIPT_MOD' && req === 'FILE_WRITE_STARTUP') {
55
+ const startupFiles = ['package.json', '.npmrc', '.bashrc', '.zshrc'];
56
+ matchedFacts = matchedFacts.filter(f => f.path && startupFiles.some(s => f.path.includes(s)));
57
+ if (matchedFacts.length === 0) return null;
58
+ }
59
+
40
60
  triggers.push(...matchedFacts.map(f => ({ ...f, type: req })));
41
61
  }
42
62
 
@@ -45,9 +45,59 @@ const RULES = [
45
45
  id: 'ZFT-006',
46
46
  alias: 'DYNAMIC_REQUIRE_DEPENDENCY',
47
47
  name: 'Dynamic Require Dependency',
48
- requires: ['DYNAMIC_EXECUTION'], // Will check if type === 'dynamic_require' in engine
48
+ requires: ['DYNAMIC_EXECUTION'],
49
49
  baseScore: 30,
50
50
  description: 'Detection of dynamic require calls where the dependency name is a variable.'
51
+ },
52
+ {
53
+ id: 'ZFT-007',
54
+ alias: 'DNS_EXFILTRATION',
55
+ name: 'DNS-Based Exfiltration',
56
+ requires: ['ENV_READ', 'NETWORK_SINK'], // Engine will check for dns.resolve in callee
57
+ baseScore: 45,
58
+ description: 'Stealthy environment variable exfiltration via DNS lookups.'
59
+ },
60
+ {
61
+ id: 'ZFT-008',
62
+ alias: 'SUSPICIOUS_COLLECTION',
63
+ name: 'Suspicious Information Collection',
64
+ requires: ['ENV_READ'],
65
+ optional: ['FILE_READ_SENSITIVE'],
66
+ baseScore: 20,
67
+ description: 'Massive environment or file reading without immediate network activity (potential harvesting).'
68
+ },
69
+ {
70
+ id: 'ZFT-009',
71
+ alias: 'REMOTE_DROPPER_PATTERN',
72
+ name: 'Remote Script Dropper',
73
+ requires: ['SHELL_EXECUTION'],
74
+ optional: ['OBFUSCATION'],
75
+ baseScore: 55,
76
+ description: 'Detection of remote script download and execution (curl | sh) patterns.'
77
+ },
78
+ {
79
+ id: 'ZFT-010',
80
+ alias: 'ENCRYPTED_EXFILTRATION',
81
+ name: 'Encrypted Data Exfiltration',
82
+ requires: ['ENCODER_USE', 'NETWORK_SINK'],
83
+ baseScore: 50,
84
+ description: 'Data being encoded/encrypted before being sent over the network.'
85
+ },
86
+ {
87
+ id: 'ZFT-011',
88
+ alias: 'RAW_SOCKET_TUNNEL',
89
+ name: 'Raw Socket Tunneling',
90
+ requires: ['NETWORK_SINK'], // Engine will check for net.connect/net.createConnection
91
+ baseScore: 45,
92
+ description: 'Use of raw network sockets instead of http/dns, often used for reverse shells.'
93
+ },
94
+ {
95
+ id: 'ZFT-012',
96
+ alias: 'STARTUP_SCRIPT_MOD',
97
+ name: 'Startup Script Modification',
98
+ requires: ['FILE_WRITE_STARTUP'], // Will check for package.json or .npmrc
99
+ baseScore: 60,
100
+ description: 'Detection of attempts to modify package.json scripts or npm configuration.'
51
101
  }
52
102
  ];
53
103