@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 +1 -1
- package/src/collector.js +17 -5
- package/src/engine.js +41 -2
- package/src/rules/definitions.js +11 -1
- package/src/scanner.js +33 -23
- package/src/shield.js +35 -6
- package/zift.json +35 -0
package/package.json
CHANGED
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('
|
|
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', '
|
|
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 = ['
|
|
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 = ['
|
|
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
|
|
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 = [];
|
package/src/rules/definitions.js
CHANGED
|
@@ -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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
+
}
|