@7nsane/zift 2.0.0 → 2.2.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.2.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
@@ -12,16 +12,23 @@ class ASTCollector {
12
12
  collect(code, filePath) {
13
13
  const facts = {
14
14
  ENV_READ: [],
15
+ MASS_ENV_ACCESS: [],
15
16
  FILE_READ_SENSITIVE: [],
16
17
  NETWORK_SINK: [],
18
+ DNS_SINK: [],
19
+ RAW_SOCKET_SINK: [],
17
20
  DYNAMIC_EXECUTION: [],
21
+ DYNAMIC_REQUIRE: [],
18
22
  OBFUSCATION: [],
19
23
  FILE_WRITE_STARTUP: [],
20
24
  SHELL_EXECUTION: [],
21
- ENCODER_USE: []
25
+ ENCODER_USE: [],
26
+ REMOTE_FETCH_SIGNAL: [],
27
+ PIPE_TO_SHELL_SIGNAL: []
22
28
  };
23
29
  const flows = [];
24
30
  const sourceCode = code;
31
+ let envAccessCount = 0;
25
32
 
26
33
  let ast;
27
34
  try {
@@ -60,16 +67,16 @@ class ASTCollector {
60
67
  }
61
68
 
62
69
  if (calleeCode === 'require' && node.arguments.length > 0 && node.arguments[0].type !== 'Literal') {
63
- facts.DYNAMIC_EXECUTION.push({
70
+ facts.DYNAMIC_REQUIRE.push({
64
71
  file: filePath,
65
72
  line: node.loc.start.line,
66
- type: 'dynamic_require',
67
73
  variable: sourceCode.substring(node.arguments[0].start, node.arguments[0].end)
68
74
  });
69
75
  }
70
76
 
71
- if (this.isNetworkSink(calleeCode)) {
72
- facts.NETWORK_SINK.push({
77
+ const netType = this.getNetworkType(calleeCode);
78
+ if (netType) {
79
+ facts[netType].push({
73
80
  file: filePath,
74
81
  line: node.loc.start.line,
75
82
  callee: calleeCode
@@ -82,6 +89,19 @@ class ASTCollector {
82
89
  line: node.loc.start.line,
83
90
  callee: calleeCode
84
91
  });
92
+
93
+ // Signal Analysis: Dropper Patterns
94
+ node.arguments.forEach(arg => {
95
+ if (arg.type === 'Literal' && typeof arg.value === 'string') {
96
+ const val = arg.value.toLowerCase();
97
+ if ((val.includes('curl') || val.includes('wget') || val.includes('fetch')) && (val.includes('http') || val.includes('//'))) {
98
+ facts.REMOTE_FETCH_SIGNAL.push({ file: filePath, line: node.loc.start.line, context: val });
99
+ }
100
+ if (val.includes('| sh') || val.includes('| bash') || val.includes('| cmd') || val.includes('| pwsh')) {
101
+ facts.PIPE_TO_SHELL_SIGNAL.push({ file: filePath, line: node.loc.start.line, context: val });
102
+ }
103
+ }
104
+ });
85
105
  }
86
106
 
87
107
  if (this.isEncoder(calleeCode)) {
@@ -100,9 +120,16 @@ class ASTCollector {
100
120
  });
101
121
  }
102
122
 
123
+ if (this.isStartupFileWrite(calleeCode, node, sourceCode)) {
124
+ facts.FILE_WRITE_STARTUP.push({
125
+ file: filePath,
126
+ line: node.loc.start.line,
127
+ path: node.arguments[0] ? sourceCode.substring(node.arguments[0].start, node.arguments[0].end) : 'unknown'
128
+ });
129
+ }
130
+
103
131
  node.arguments.forEach((arg, index) => {
104
132
  const argCode = sourceCode.substring(arg.start, arg.end);
105
- // Improved check: Does the expression contain any variable we know is tainted?
106
133
  const isArgTainted = argCode.includes('process.env') || flows.some(f => {
107
134
  const regex = new RegExp(`\\b${f.toVar}\\b`);
108
135
  return regex.test(argCode);
@@ -129,11 +156,16 @@ class ASTCollector {
129
156
  const whitelist = ['NODE_ENV', 'TIMING', 'DEBUG', 'VERBOSE', 'CI', 'APPDATA', 'HOME', 'USERPROFILE', 'PATH', 'PWD'];
130
157
  if (whitelist.includes(property)) return;
131
158
 
159
+ envAccessCount++;
132
160
  facts.ENV_READ.push({
133
161
  file: filePath,
134
162
  line: node.loc.start.line,
135
163
  variable: property ? `process.env.${property}` : 'process.env'
136
164
  });
165
+
166
+ if (envAccessCount > 5) {
167
+ facts.MASS_ENV_ACCESS.push({ file: filePath, line: node.loc.start.line, count: envAccessCount });
168
+ }
137
169
  }
138
170
  },
139
171
  VariableDeclarator: (node) => {
@@ -183,20 +215,21 @@ class ASTCollector {
183
215
  return { facts, flows };
184
216
  }
185
217
 
186
- isNetworkSink(calleeCode) {
187
- const methodSinks = [
188
- 'http.request', 'https.request', 'http.get', 'https.get',
189
- 'net.connect', 'dns.lookup', 'dns.resolve', 'dns.resolve4', 'dns.resolve6',
190
- 'fetch', 'axios', 'request'
191
- ];
192
- // Improved matching for require('https').get patterns
193
- return methodSinks.some(sink => {
218
+ getNetworkType(calleeCode) {
219
+ const dnsSinks = ['dns.lookup', 'dns.resolve', 'dns.resolve4', 'dns.resolve6'];
220
+ const rawSocketSinks = ['net.connect', 'net.createConnection'];
221
+ const networkSinks = ['http.request', 'https.request', 'http.get', 'https.get', 'fetch', 'axios', 'request'];
222
+
223
+ if (dnsSinks.some(sink => calleeCode === sink || calleeCode.endsWith('.' + sink))) return 'DNS_SINK';
224
+ if (rawSocketSinks.some(sink => calleeCode === sink || calleeCode.endsWith('.' + sink))) return 'RAW_SOCKET_SINK';
225
+ if (networkSinks.some(sink => {
194
226
  if (calleeCode === sink) return true;
195
227
  if (calleeCode.endsWith('.' + sink)) return true;
196
- // Catch cases like require('https').get
197
228
  if (sink.includes('.') && calleeCode.endsWith(sink.split('.')[1]) && calleeCode.includes(sink.split('.')[0])) return true;
198
229
  return false;
199
- }) && !calleeCode.includes('IdleCallback');
230
+ })) return 'NETWORK_SINK';
231
+
232
+ return null;
200
233
  }
201
234
 
202
235
  isShellSink(calleeCode) {
@@ -210,7 +243,7 @@ class ASTCollector {
210
243
  }
211
244
 
212
245
  isEncoder(calleeCode) {
213
- const encoders = ['Buffer.from', 'btoa', 'atob'];
246
+ const encoders = ['Buffer.from', 'btoa', 'atob', 'zlib.deflate', 'zlib.gzip', 'crypto.createCipheriv'];
214
247
  return encoders.some(enc => calleeCode === enc || calleeCode.endsWith('.' + enc));
215
248
  }
216
249
 
@@ -235,12 +268,24 @@ class ASTCollector {
235
268
 
236
269
  if (node.arguments.length > 0 && node.arguments[0].type === 'Literal') {
237
270
  const pathValue = String(node.arguments[0].value);
238
- const sensitive = ['.ssh', '.env', 'shadow', 'passwd', 'credentials', 'token'];
271
+ const sensitive = ['.ssh', '.env', 'shadow', 'passwd', 'credentials', 'token', '_netrc', 'aws_access_key'];
239
272
  return sensitive.some((s) => pathValue.toLowerCase().includes(s));
240
273
  }
241
274
  return false;
242
275
  }
243
276
 
277
+ isStartupFileWrite(calleeCode, node, sourceCode) {
278
+ if (!calleeCode.includes('fs.writeFile') && !calleeCode.includes('fs.writeFileSync') &&
279
+ !calleeCode.includes('fs.appendFile')) return false;
280
+
281
+ if (node.arguments.length > 0 && node.arguments[0].type === 'Literal') {
282
+ const pathValue = String(node.arguments[0].value);
283
+ const startup = ['package.json', '.npmrc', '.bashrc', '.zshrc', 'crontab', 'init.d', 'systemd'];
284
+ return startup.some((s) => pathValue.toLowerCase().includes(s));
285
+ }
286
+ return false;
287
+ }
288
+
244
289
  getSourceCode(node) {
245
290
  return this.sourceCode.substring(node.start, node.end);
246
291
  }
package/src/engine.js CHANGED
@@ -6,7 +6,7 @@ class SafetyEngine {
6
6
  }
7
7
 
8
8
  evaluate(packageFacts, lifecycleFiles) {
9
- const findings = [];
9
+ let findings = [];
10
10
 
11
11
  // Process each rule
12
12
  for (const rule of RULES) {
@@ -16,6 +16,9 @@ class SafetyEngine {
16
16
  }
17
17
  }
18
18
 
19
+ // Sort by score (desc) and then by priority (desc)
20
+ findings.sort((a, b) => (b.score - a.score) || (b.priority - a.priority));
21
+
19
22
  return findings;
20
23
  }
21
24
 
@@ -29,12 +32,9 @@ class SafetyEngine {
29
32
  for (const req of rule.requires) {
30
33
  let matchedFacts = facts[req] || [];
31
34
 
32
- // Special case for dynamic require (which shares DYNAMIC_EXECUTION fact type)
33
- if (rule.alias === 'DYNAMIC_REQUIRE_DEPENDENCY') {
34
- matchedFacts = matchedFacts.filter(f => f.type === 'dynamic_require');
35
- } else if (req === 'DYNAMIC_EXECUTION') {
36
- matchedFacts = matchedFacts.filter(f => f.type !== 'dynamic_require');
37
- }
35
+ // Specialist Rule: Startup Mod (ZFT-012) requires specific file paths (now explicit in definitions but engine may still help)
36
+ // But per review, we should aim for explicit facts.
37
+ // ZFT-012 now just requires FILE_WRITE_STARTUP. Simple.
38
38
 
39
39
  if (matchedFacts.length === 0) return null; // Rule not matched
40
40
  triggers.push(...matchedFacts.map(f => ({ ...f, type: req })));
@@ -51,10 +51,10 @@ class SafetyEngine {
51
51
  }
52
52
  }
53
53
 
54
- // Apply Lifecycle Multiplier (2.0x for V2)
55
- const isInLifecycle = triggers.some(t => lifecycleFiles.has(t.file));
54
+ // Check for Lifecycle Context Fact (Virtual or Actual)
55
+ const isInLifecycle = triggers.some(t => lifecycleFiles.has(t.file)) || (facts['LIFECYCLE_CONTEXT'] && facts['LIFECYCLE_CONTEXT'].length > 0);
56
56
  if (isInLifecycle) {
57
- multiplier = 2.0;
57
+ multiplier *= 2.0;
58
58
  }
59
59
 
60
60
  // Encoder Multiplier (1.5x)
@@ -64,18 +64,18 @@ class SafetyEngine {
64
64
  }
65
65
 
66
66
  // Cluster Bonus: Source + Sink
67
- const hasSource = triggers.some(t => t.type.includes('READ'));
68
- const hasSink = triggers.some(t => t.type.includes('SINK') || t.type === 'DYNAMIC_EXECUTION' || t.type === 'SHELL_EXECUTION');
67
+ const hasSource = triggers.some(t => t.type.includes('READ') || t.type.includes('ACCESS'));
68
+ const hasSink = triggers.some(t => t.type.includes('SINK') || t.type === 'DYNAMIC_EXECUTION' || t.type === 'SHELL_EXECUTION' || t.type === 'DYNAMIC_REQUIRE');
69
69
  if (hasSource && hasSink) {
70
70
  baseScore += 40;
71
71
  }
72
72
 
73
73
  let finalScore = baseScore * multiplier;
74
74
 
75
- // Severe Cluster: ENV_READ + (NETWORK_SINK | SHELL_EXECUTION) + lifecycleContext = Critical (100)
76
- const isEnvRead = triggers.some(t => t.type === 'ENV_READ');
77
- const isDangerousSink = triggers.some(t => t.type === 'NETWORK_SINK' || t.type === 'SHELL_EXECUTION');
78
- if (isEnvRead && isDangerousSink && isInLifecycle) {
75
+ // Severe Cluster: SENSITIVE_READ + Dangerous Sink + lifecycleContext = Critical (100)
76
+ const isSensitiveRead = triggers.some(t => t.type === 'ENV_READ' || t.type === 'FILE_READ_SENSITIVE');
77
+ const isDangerousSink = triggers.some(t => t.type === 'NETWORK_SINK' || t.type === 'DNS_SINK' || t.type === 'RAW_SOCKET_SINK' || t.type === 'SHELL_EXECUTION');
78
+ if (isSensitiveRead && isDangerousSink && isInLifecycle) {
79
79
  finalScore = 100;
80
80
  }
81
81
 
@@ -83,6 +83,7 @@ class SafetyEngine {
83
83
  id: rule.id,
84
84
  alias: rule.alias,
85
85
  name: rule.name,
86
+ priority: rule.priority || 1,
86
87
  score: Math.min(finalScore, 100),
87
88
  triggers: triggers,
88
89
  description: rule.description,
@@ -5,6 +5,7 @@ const RULES = [
5
5
  name: 'Environment Variable Exfiltration',
6
6
  requires: ['ENV_READ', 'NETWORK_SINK'],
7
7
  optional: ['OBFUSCATION'],
8
+ priority: 1,
8
9
  baseScore: 40,
9
10
  description: 'Detection of environment variables being read and sent over the network.'
10
11
  },
@@ -13,6 +14,7 @@ const RULES = [
13
14
  alias: 'SENSITIVE_FILE_EXFILTRATION',
14
15
  name: 'Sensitive File Exfiltration',
15
16
  requires: ['FILE_READ_SENSITIVE', 'NETWORK_SINK'],
17
+ priority: 1,
16
18
  baseScore: 50,
17
19
  description: 'Detection of sensitive files (e.g., .ssh, .env) being read and sent over the network.'
18
20
  },
@@ -21,6 +23,7 @@ const RULES = [
21
23
  alias: 'PERSISTENCE_ATTEMPT',
22
24
  name: 'Persistence Attempt',
23
25
  requires: ['FILE_WRITE_STARTUP'],
26
+ priority: 2,
24
27
  baseScore: 60,
25
28
  description: 'Detection of attempts to write to system startup directories.'
26
29
  },
@@ -29,6 +32,7 @@ const RULES = [
29
32
  alias: 'OBFUSCATED_EXECUTION',
30
33
  name: 'Obfuscated Execution',
31
34
  requires: ['OBFUSCATION', 'DYNAMIC_EXECUTION'],
35
+ priority: 2,
32
36
  baseScore: 40,
33
37
  description: 'Detection of high-entropy strings being executed via eval or Function constructor.'
34
38
  },
@@ -38,6 +42,7 @@ const RULES = [
38
42
  name: 'Shell Command Execution',
39
43
  requires: ['SHELL_EXECUTION'],
40
44
  optional: ['ENV_READ', 'FILE_READ_SENSITIVE'],
45
+ priority: 1,
41
46
  baseScore: 50,
42
47
  description: 'Detection of shell command execution (child_process).'
43
48
  },
@@ -45,17 +50,75 @@ const RULES = [
45
50
  id: 'ZFT-006',
46
51
  alias: 'DYNAMIC_REQUIRE_DEPENDENCY',
47
52
  name: 'Dynamic Require Dependency',
48
- requires: ['DYNAMIC_EXECUTION'], // Will check if type === 'dynamic_require' in engine
53
+ requires: ['DYNAMIC_REQUIRE'],
54
+ priority: 1,
49
55
  baseScore: 30,
50
56
  description: 'Detection of dynamic require calls where the dependency name is a variable.'
57
+ },
58
+ {
59
+ id: 'ZFT-007',
60
+ alias: 'DNS_EXFILTRATION',
61
+ name: 'DNS-Based Exfiltration',
62
+ requires: ['ENV_READ', 'DNS_SINK'],
63
+ priority: 2,
64
+ baseScore: 45,
65
+ description: 'Stealthy environment variable exfiltration via DNS lookups.'
66
+ },
67
+ {
68
+ id: 'ZFT-008',
69
+ alias: 'SUSPICIOUS_COLLECTION',
70
+ name: 'Suspicious Information Collection',
71
+ requires: ['MASS_ENV_ACCESS'],
72
+ optional: ['FILE_READ_SENSITIVE'],
73
+ priority: 1,
74
+ baseScore: 20,
75
+ description: 'Massive environment reading without immediate network activity (potential harvesting).'
76
+ },
77
+ {
78
+ id: 'ZFT-009',
79
+ alias: 'REMOTE_DROPPER_PATTERN',
80
+ name: 'Remote Script Dropper',
81
+ requires: ['SHELL_EXECUTION', 'REMOTE_FETCH_SIGNAL'],
82
+ optional: ['OBFUSCATION', 'PIPE_TO_SHELL_SIGNAL'],
83
+ priority: 3,
84
+ baseScore: 55,
85
+ description: 'Detection of remote script download and execution (curl | sh) patterns.'
86
+ },
87
+ {
88
+ id: 'ZFT-010',
89
+ alias: 'ENCODED_EXFILTRATION',
90
+ name: 'Encoded Data Exfiltration',
91
+ requires: ['ENV_READ', 'NETWORK_SINK', 'ENCODER_USE'],
92
+ priority: 3,
93
+ baseScore: 70,
94
+ description: 'Sensitive data encoded before transmission to evade detection.'
95
+ },
96
+ {
97
+ id: 'ZFT-011',
98
+ alias: 'RAW_SOCKET_TUNNEL',
99
+ name: 'Raw Socket Tunneling',
100
+ requires: ['RAW_SOCKET_SINK'],
101
+ priority: 2,
102
+ baseScore: 45,
103
+ description: 'Use of raw network sockets instead of http/dns, often used for reverse shells.'
104
+ },
105
+ {
106
+ id: 'ZFT-012',
107
+ alias: 'STARTUP_SCRIPT_MOD',
108
+ name: 'Startup Script Modification',
109
+ requires: ['FILE_WRITE_STARTUP'],
110
+ priority: 2,
111
+ baseScore: 60,
112
+ description: 'Detection of attempts to modify package.json scripts or npm configuration.'
51
113
  }
52
114
  ];
53
115
 
54
116
  const CATEGORIES = {
55
- SOURCES: ['ENV_READ', 'FILE_READ_SENSITIVE'],
56
- SINKS: ['NETWORK_SINK', 'DYNAMIC_EXECUTION', 'SHELL_EXECUTION'],
57
- SIGNALS: ['OBFUSCATION', 'ENCODER_USE'],
58
- PERSISTENCE: ['FILE_WRITE_STARTUP']
117
+ SOURCES: ['ENV_READ', 'FILE_READ_SENSITIVE', 'MASS_ENV_ACCESS'],
118
+ 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'],
120
+ PERSISTENCE: ['FILE_WRITE_STARTUP'],
121
+ CONTEXT: ['LIFECYCLE_CONTEXT']
59
122
  };
60
123
 
61
124
  module.exports = { RULES, CATEGORIES };
package/src/scanner.js CHANGED
@@ -27,13 +27,20 @@ class PackageScanner {
27
27
  let allFacts = {
28
28
  facts: {
29
29
  ENV_READ: [],
30
+ MASS_ENV_ACCESS: [],
30
31
  FILE_READ_SENSITIVE: [],
31
32
  NETWORK_SINK: [],
33
+ DNS_SINK: [],
34
+ RAW_SOCKET_SINK: [],
32
35
  DYNAMIC_EXECUTION: [],
36
+ DYNAMIC_REQUIRE: [],
33
37
  OBFUSCATION: [],
34
38
  FILE_WRITE_STARTUP: [],
35
39
  SHELL_EXECUTION: [],
36
- ENCODER_USE: []
40
+ ENCODER_USE: [],
41
+ REMOTE_FETCH_SIGNAL: [],
42
+ PIPE_TO_SHELL_SIGNAL: [],
43
+ LIFECYCLE_CONTEXT: []
37
44
  },
38
45
  flows: []
39
46
  };
@@ -78,6 +85,11 @@ class PackageScanner {
78
85
  }
79
86
 
80
87
  // Merge facts (Synchronized)
88
+ if (lifecycleFiles.has(file)) {
89
+ facts.LIFECYCLE_CONTEXT = facts.LIFECYCLE_CONTEXT || [];
90
+ facts.LIFECYCLE_CONTEXT.push({ file, reason: 'Lifecycle script context detected' });
91
+ }
92
+
81
93
  for (const category in facts) {
82
94
  if (allFacts.facts[category]) {
83
95
  allFacts.facts[category].push(...facts[category]);
package/v22_output.txt ADDED
@@ -0,0 +1,12 @@
1
+ 🛡️ Verifying Zift v2.2.0 Deterministic Architecture...
2
+ 📊 Audit Results:
3
+ - [Priority: 1, Score: 100] ZFT-005: Shell Command Execution
4
+ - [Priority: 1, Score: 80] ZFT-001: Environment Variable Exfiltration
5
+
6
+ 🔍 Structural Verification:
7
+ - DNS Exfil detected: false
8
+ - Env Exfil detected: true
9
+ - Remote Dropper detected: false
10
+ - Mass Env Access detected: false
11
+ - Dynamic Require detected: false
12
+ ❌ v2.2.0 Verification incomplete. Some rules did not trigger as expected.
@@ -0,0 +1,16 @@
1
+ 🛡️ Verifying Zift v2.2.0 Deterministic Architecture...
2
+ 📊 Audit Results:
3
+ - [Priority: 1, Score: 100] ZFT-005: Shell Command Execution
4
+ - [Priority: 2, Score: 85] ZFT-007: DNS-Based Exfiltration
5
+ - [Priority: 1, Score: 80] ZFT-001: Environment Variable Exfiltration
6
+ - [Priority: 3, Score: 75] ZFT-009: Remote Script Dropper
7
+ - [Priority: 1, Score: 30] ZFT-006: Dynamic Require Dependency
8
+ - [Priority: 1, Score: 20] ZFT-008: Suspicious Information Collection
9
+
10
+ 🔍 Structural Verification:
11
+ - DNS Exfil detected: true
12
+ - Env Exfil detected: true
13
+ - Remote Dropper detected: true
14
+ - Mass Env Access detected: true
15
+ - Dynamic Require detected: true
16
+ ✅ v2.2.0 Architecture Verified Successfully!