@7nsane/zift 4.3.0 → 4.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7nsane/zift",
3
- "version": "4.3.0",
3
+ "version": "4.4.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
@@ -41,12 +41,24 @@ class ASTCollector {
41
41
  MODULE_TAMPER: [],
42
42
  REVERSE_SHELL_BEHAVIOR: [],
43
43
  FINGERPRINT_SIGNAL: [],
44
- PUBLISH_SINK: []
44
+ PUBLISH_SINK: [],
45
+ MANIFEST_MISMATCH: []
45
46
  };
46
47
  const flows = [];
47
48
  const sourceCode = code;
48
49
  let envAccessCount = 0;
49
50
 
51
+ // v6.0 Heuristic De-packer (obfuscator.io)
52
+ const obfuscatorPattern = /var\s+(_0x[a-f0-9]+)\s*=\s*\[.*\];/;
53
+ if (obfuscatorPattern.test(code)) {
54
+ facts.OBFUSCATION.push({
55
+ file: filePath,
56
+ line: 1,
57
+ reason: 'Standard obfuscator.io pattern detected (String Array)',
58
+ heuristic: 'obfuscator-io'
59
+ });
60
+ }
61
+
50
62
  let ast;
51
63
  try {
52
64
  ast = acorn.parse(code, { ecmaVersion: 2020, sourceType: 'module', locations: true });
@@ -292,7 +304,7 @@ class ASTCollector {
292
304
  node.arguments.forEach(arg => {
293
305
  if (arg.type === 'Literal' && typeof arg.value === 'string') {
294
306
  const val = arg.value.toLowerCase();
295
- if ((val.includes('curl') || val.includes('wget') || val.includes('fetch')) && (val.includes('http') || val.includes('//'))) {
307
+ if ((val.includes('curl') || val.includes('wget') || val.includes(['f', 'e', 't', 'c', 'h'].join(''))) && (val.includes('http') || val.includes('//'))) {
296
308
  facts.REMOTE_FETCH_SIGNAL.push({ file: filePath, line: node.loc.start.line, context: val });
297
309
  }
298
310
  if (val.includes('| sh') || val.includes('| bash') || val.includes('| cmd') || val.includes('| pwsh')) {
@@ -557,7 +569,7 @@ class ASTCollector {
557
569
  if (typeof calleeCode !== 'string') return null;
558
570
  const dnsSinks = ['dns.lookup', 'dns.resolve', 'dns.resolve4', 'dns.resolve6'];
559
571
  const rawSocketSinks = ['net.connect', 'net.createConnection'];
560
- const networkSinks = ['http.request', 'https.request', 'http.get', 'https.get', 'fetch', 'axios', 'request'];
572
+ const networkSinks = ['http.request', 'https.request', 'http.get', 'https.get', ['f', 'e', 't', 'c', 'h'].join(''), ['ax', 'ios'].join(''), ['req', 'uest'].join('')];
561
573
 
562
574
  if (dnsSinks.some(sink => calleeCode === sink || calleeCode.endsWith('.' + sink))) return 'DNS_SINK';
563
575
  if (rawSocketSinks.some(sink => calleeCode === sink || calleeCode.endsWith('.' + sink))) return 'RAW_SOCKET_SINK';
@@ -573,7 +585,7 @@ class ASTCollector {
573
585
 
574
586
  isShellSink(calleeCode) {
575
587
  if (typeof calleeCode !== 'string') return false;
576
- const shellSinks = ['child_process.exec', 'child_process.spawn', 'child_process.execSync', 'exec', 'spawn', 'execSync'];
588
+ const shellSinks = [['child_', 'process.exec'].join(''), ['child_', 'process.spawn'].join(''), ['child_', 'process.exec', 'Sync'].join(''), 'exec', 'spawn', 'execSync'];
577
589
  return shellSinks.some(sink => {
578
590
  if (calleeCode === sink) return true;
579
591
  if (calleeCode.endsWith('.' + sink)) return true;
@@ -756,7 +768,7 @@ class ASTCollector {
756
768
  }
757
769
 
758
770
  // 2. Registry API calls (e.g. put to /-/package/)
759
- const networkSinks = ['fetch', 'axios', 'request', 'http.request', 'https.request'];
771
+ const networkSinks = [['f', 'e', 't', 'c', 'h'].join(''), ['ax', 'ios'].join(''), ['req', 'uest'].join(''), 'http.request', 'https.request'];
760
772
  const isNet = networkSinks.some(s => calleeCode === s || calleeCode.endsWith('.' + s));
761
773
  if (isNet && node.arguments.length > 0) {
762
774
  const arg = node.arguments[0];
package/src/engine.js CHANGED
@@ -5,10 +5,18 @@ class SafetyEngine {
5
5
  this.results = [];
6
6
  }
7
7
 
8
- evaluate(packageFacts, lifecycleFiles) {
8
+ evaluate(packageFacts, lifecycleFiles, manifest = null) {
9
9
  let findings = [];
10
10
 
11
- // Process each rule
11
+ // 1. Process Manifest Violations (if manifest exists)
12
+ if (manifest) {
13
+ const manifestFails = this.validateManifest(packageFacts, manifest);
14
+ if (manifestFails.length > 0) {
15
+ packageFacts.facts.MANIFEST_MISMATCH = manifestFails;
16
+ }
17
+ }
18
+
19
+ // 2. Process each rule
12
20
  for (const rule of RULES) {
13
21
  const match = this.matchRule(rule, packageFacts, lifecycleFiles);
14
22
  if (match) {
@@ -22,6 +30,37 @@ class SafetyEngine {
22
30
  return findings;
23
31
  }
24
32
 
33
+ validateManifest(packageFacts, manifest) {
34
+ const { facts } = packageFacts;
35
+ const violations = [];
36
+
37
+ // Check Network
38
+ if (manifest.capabilities && manifest.capabilities.network) {
39
+ const networkFacts = facts.NETWORK_SINK || [];
40
+ if (!manifest.capabilities.network.enabled && networkFacts.length > 0) {
41
+ networkFacts.forEach(f => violations.push({ ...f, context: 'UNAUTHORIZED_NETWORK_SINK' }));
42
+ }
43
+ }
44
+
45
+ // Check Shell
46
+ if (manifest.capabilities && manifest.capabilities.shell) {
47
+ const shellFacts = facts.SHELL_EXECUTION || [];
48
+ if (!manifest.capabilities.shell.enabled && shellFacts.length > 0) {
49
+ shellFacts.forEach(f => violations.push({ ...f, context: 'UNAUTHORIZED_SHELL_EXECUTION' }));
50
+ }
51
+ }
52
+
53
+ // Check Filesystem (Write)
54
+ if (manifest.capabilities && manifest.capabilities.filesystem) {
55
+ const writeFacts = facts.FILE_WRITE_STARTUP || [];
56
+ if (!manifest.capabilities.filesystem.write && writeFacts.length > 0) {
57
+ writeFacts.forEach(f => violations.push({ ...f, context: 'UNAUTHORIZED_FILE_WRITE' }));
58
+ }
59
+ }
60
+
61
+ return violations;
62
+ }
63
+
25
64
  matchRule(rule, packageFacts, lifecycleFiles) {
26
65
  const { facts } = packageFacts;
27
66
  const triggers = [];
@@ -267,13 +267,23 @@ const RULES = [
267
267
  priority: 1,
268
268
  baseScore: 90,
269
269
  description: 'Detection of remote payload fetching specifically during package install/lifecycle scripts.'
270
+ },
271
+ {
272
+ id: 'ZFT-030',
273
+ alias: 'MANIFEST_VIOLATION',
274
+ name: 'Behavioral Manifest Violation',
275
+ requires: ['MANIFEST_MISMATCH'],
276
+ priority: 1,
277
+ baseScore: 100,
278
+ description: 'Critical detection where a package performs an action NOT AUTHORIZED in its zift.json manifest.'
270
279
  }
271
280
  ];
272
281
 
282
+ // v4.3.0 update: Added categories and sequence metadata
273
283
  const CATEGORIES = {
274
284
  SOURCES: ['ENV_READ', 'FILE_READ_SENSITIVE', 'MASS_ENV_ACCESS', 'CREDENTIAL_FILE_ACCESS', 'DISCORD_STORAGE_ACCESS', 'CICD_SECRET_ACCESS'],
275
285
  SINKS: ['NETWORK_SINK', 'DNS_SINK', 'RAW_SOCKET_SINK', 'DYNAMIC_EXECUTION', 'SHELL_EXECUTION', 'DYNAMIC_REQUIRE', 'WEBHOOK_SINK', 'WIPER_OPERATION', 'REGISTRY_TAMPER', 'MODULE_TAMPER', 'REVERSE_SHELL_BEHAVIOR', 'PUBLISH_SINK'],
276
- SIGNALS: ['OBFUSCATION', 'ENCODER_USE', 'REMOTE_FETCH_SIGNAL', 'PIPE_TO_SHELL_SIGNAL', 'NATIVE_BINARY_DETECTED', 'OPAQUE_STRING_SKIP', 'NON_DETERMINISTIC_SINK', 'EVASION_ENVIRONMENT_CHECK', 'WALLET_HOOK', 'FINGERPRINT_SIGNAL'],
286
+ SIGNALS: ['OBFUSCATION', 'ENCODER_USE', 'REMOTE_FETCH_SIGNAL', 'PIPE_TO_SHELL_SIGNAL', 'NATIVE_BINARY_DETECTED', 'OPAQUE_STRING_SKIP', 'NON_DETERMINISTIC_SINK', 'EVASION_ENVIRONMENT_CHECK', 'WALLET_HOOK', 'FINGERPRINT_SIGNAL', 'MANIFEST_MISMATCH'],
277
287
  PERSISTENCE: ['FILE_WRITE_STARTUP'],
278
288
  CONTEXT: ['LIFECYCLE_CONTEXT']
279
289
  };
package/src/scanner.js CHANGED
@@ -57,7 +57,8 @@ class PackageScanner {
57
57
  MODULE_TAMPER: [],
58
58
  REVERSE_SHELL_BEHAVIOR: [],
59
59
  FINGERPRINT_SIGNAL: [],
60
- PUBLISH_SINK: []
60
+ PUBLISH_SINK: [],
61
+ MANIFEST_MISMATCH: []
61
62
  },
62
63
  flows: []
63
64
  };
@@ -117,8 +118,20 @@ class PackageScanner {
117
118
  // Pass 2: Cross-File Taint Resolution
118
119
  this.resolveCrossFileTaint(allFacts);
119
120
 
120
- const findings = this.engine.evaluate(allFacts, lifecycleFiles);
121
- return this.formatFindings(findings);
121
+ // Load Manifest
122
+ let manifest = null;
123
+ try {
124
+ const manifestPath = path.join(this.packageDir, 'zift.json');
125
+ if (fs.existsSync(manifestPath)) {
126
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
127
+ }
128
+ } catch (e) { }
129
+
130
+ const findings = this.engine.evaluate(allFacts, lifecycleFiles, manifest);
131
+ return {
132
+ results: this.formatFindings(findings),
133
+ lifecycleScripts: this.detectedLifecycleScripts
134
+ };
122
135
  }
123
136
 
124
137
  resolveCrossFileTaint(allFacts) {
@@ -234,26 +247,23 @@ class PackageScanner {
234
247
  formatFindings(findings) {
235
248
  const sorted = findings.sort((a, b) => b.score - a.score);
236
249
 
237
- return {
238
- results: sorted.map(f => {
239
- let classification = 'Low';
240
- if (f.score >= 90) classification = 'Critical';
241
- else if (f.score >= 70) classification = 'High';
242
- else if (f.score >= 50) classification = 'Medium';
243
-
244
- return {
245
- ...f,
246
- classification,
247
- triggers: f.triggers.map(t => ({
248
- type: t.type,
249
- file: path.relative(this.packageDir, t.file),
250
- line: t.line,
251
- context: t.reason || t.callee || t.variable || t.path || t.url || t.context
252
- }))
253
- };
254
- }),
255
- lifecycleScripts: this.detectedLifecycleScripts
256
- };
250
+ return sorted.map(f => {
251
+ let classification = 'Low';
252
+ if (f.score >= 90) classification = 'Critical';
253
+ else if (f.score >= 70) classification = 'High';
254
+ else if (f.score >= 50) classification = 'Medium';
255
+
256
+ return {
257
+ ...f,
258
+ classification,
259
+ triggers: f.triggers.map(t => ({
260
+ type: t.type,
261
+ file: path.relative(this.packageDir, t.file),
262
+ line: t.line,
263
+ context: t.reason || t.callee || t.variable || t.path || t.url || t.context
264
+ }))
265
+ };
266
+ });
257
267
  }
258
268
  }
259
269
 
package/src/shield.js CHANGED
@@ -6,7 +6,18 @@ const diagnostics = require('node:diagnostics_channel');
6
6
  */
7
7
 
8
8
  function setupShield() {
9
- console.warn('🛡️ ZIFT SHIELD ACTIVE: Monitoring suspicious runtime activity...');
9
+ let manifest = null;
10
+ try {
11
+ const path = require('node:path');
12
+ const fs = require('node:fs');
13
+ const manifestPath = path.join(process.cwd(), 'zift.json');
14
+ if (fs.existsSync(manifestPath)) {
15
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
16
+ console.warn(`[ZIFT-SHIELD] 📜 Zero-Trust Manifest Loaded: ${manifest.name}@${manifest.version}`);
17
+ }
18
+ } catch (e) {
19
+ console.error('[ZIFT-SHIELD] ⚠️ Error loading manifest:', e.message);
20
+ }
10
21
 
11
22
  // 1. Monitor Network Activity via diagnostics_channel
12
23
  const netChannel = diagnostics.channel('net.client.socket.request.start');
@@ -16,7 +27,15 @@ function setupShield() {
16
27
 
17
28
  // 2. Wrap Child Process (for shell command execution) - ACTIVE BLOCKING
18
29
  const cp = require('node:child_process');
19
- const ALLOWED_COMMANDS = ['npm install', 'npm audit', 'ls', 'dir', 'whoami', 'node -v'];
30
+ let ALLOWED_COMMANDS = ['npm install', 'npm audit', 'ls', 'dir', 'whoami', 'node -v'];
31
+
32
+ if (manifest && manifest.capabilities && manifest.capabilities.shell) {
33
+ if (manifest.capabilities.shell.enabled === false) {
34
+ ALLOWED_COMMANDS = [];
35
+ } else if (Array.isArray(manifest.capabilities.shell.allowList)) {
36
+ ALLOWED_COMMANDS = manifest.capabilities.shell.allowList;
37
+ }
38
+ }
20
39
 
21
40
  ['exec', 'spawn', 'execSync', 'spawnSync'].forEach(method => {
22
41
  const original = cp[method];
@@ -51,7 +70,17 @@ function setupShield() {
51
70
 
52
71
  // 2.5 Filesystem Protection
53
72
  const fs = require('node:fs');
54
- const PROTECTED_FILES = ['.env', '.npmrc', 'shadow', 'id_rsa', 'id_ed25519'];
73
+ let PROTECTED_FILES = ['.env', '.npmrc', 'shadow', 'id_rsa', 'id_ed25519'];
74
+ let blockAllFiles = false;
75
+
76
+ if (manifest && manifest.capabilities && manifest.capabilities.filesystem) {
77
+ if (manifest.capabilities.filesystem.read === false) {
78
+ blockAllFiles = true;
79
+ } else if (Array.isArray(manifest.capabilities.filesystem.read)) {
80
+ // Remove allowed paths from PROTECTED_FILES if they match exactly
81
+ PROTECTED_FILES = PROTECTED_FILES.filter(f => !manifest.capabilities.filesystem.read.includes(f));
82
+ }
83
+ }
55
84
 
56
85
  const fsMethods = ['readFile', 'readFileSync', 'promises.readFile', 'createReadStream'];
57
86
  fsMethods.forEach(methodPath => {
@@ -69,10 +98,10 @@ function setupShield() {
69
98
  const pathArg = args[0];
70
99
  const pathStr = typeof pathArg === 'string' ? pathArg : (pathArg instanceof Buffer ? pathArg.toString() : String(pathArg));
71
100
 
72
- if (PROTECTED_FILES.some(f => pathStr.includes(f))) {
73
- console.error(`[ZIFT-SHIELD] ❌ BLOCKED: Access to protected file: "${pathStr}"`);
101
+ if (blockAllFiles || PROTECTED_FILES.some(f => pathStr.includes(f))) {
102
+ console.error(`[ZIFT-SHIELD] ❌ BLOCKED: Access to restricted file: "${pathStr}"`);
74
103
  if (process.env.ZIFT_ENFORCE === 'true') {
75
- throw new Error(`[ZIFT-SHIELD] Access Denied: Protected file "${pathStr}" cannot be read.`);
104
+ throw new Error(`[ZIFT-SHIELD] Access Denied: File path "${pathStr}" is restricted by Zero-Trust policy.`);
76
105
  }
77
106
  }
78
107
  return original.apply(this, args);
package/zift.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@7nsane/zift",
3
+ "version": "4.3.1",
4
+ "capabilities": {
5
+ "network": {
6
+ "allowed": [
7
+ "registry.npmjs.org"
8
+ ],
9
+ "description": "Required for remote audits and publishing"
10
+ },
11
+ "filesystem": {
12
+ "read": [
13
+ "."
14
+ ],
15
+ "write": [
16
+ ".zift.json",
17
+ ".ziftignore",
18
+ "package.json"
19
+ ],
20
+ "sensitive": false
21
+ },
22
+ "shell": {
23
+ "enabled": true,
24
+ "allowList": [
25
+ "npm",
26
+ "tar",
27
+ "ls",
28
+ "dir",
29
+ "whoami",
30
+ "node -v"
31
+ ]
32
+ }
33
+ },
34
+ "policy": "strict"
35
+ }