@7nsane/zift 1.1.0 → 2.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,6 +1,6 @@
1
- # Zift šŸ›”ļø
1
+ # šŸ›”ļø Zift (v2.0.0)
2
2
 
3
- **Zift** is an elite, high-performance security scanner designed to detect malicious patterns in npm packages before they are executed. By using deterministic AST analysis and lightweight variable propagation, Zift identifies potential credential exfiltration, malicious persistence, and obfuscated execution with extreme precision.
3
+ **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
4
 
5
5
  ## Installation
6
6
 
@@ -23,6 +23,20 @@ zift setup
23
23
  npm install <package-name> --zift
24
24
  ```
25
25
 
26
+ ## šŸ” Limitations & Blind Spots (v2.0.0)
27
+
28
+ To maintain a zero-false-positive baseline and high performance, Zift v2 focus on **deterministic behavioral patterns**. It does NOT currently cover:
29
+
30
+ - **Cross-file Taint**: Taint tracking is limited to intra-file propagation.
31
+ - **Runtime Decryption**: Logic that decrypts and executes memory-only payloads at runtime.
32
+ - **VM-based Execution**: Malicious payloads executed inside isolated virtual machine environments.
33
+ - **Multi-stage Loaders**: Sophisticated multi-hop obfuscation that reconstructs logic over several cycles.
34
+ - **Post-install Generation**: Malicious code generated or downloaded *after* the initial install/preinstall phase.
35
+
36
+ **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.
37
+
38
+ ## License
39
+ MIT
26
40
  ## Usage
27
41
 
28
42
  ### šŸš€ Secure Installer Mode
package/bin/zift.js CHANGED
@@ -12,22 +12,34 @@ async function main() {
12
12
  let target = '.';
13
13
  let format = 'text';
14
14
  let isInstallMode = false;
15
+ let installer = 'npm';
15
16
 
17
+ // 1. Setup & Init Commands
16
18
  if (args[0] === 'setup') {
17
19
  await runSetup();
18
20
  return;
19
21
  }
22
+ if (args[0] === 'init') {
23
+ runInit();
24
+ return;
25
+ }
26
+
27
+ // 2. Detection for bun/pnpm usage
28
+ if (args.includes('--bun')) installer = 'bun';
29
+ if (args.includes('--pnpm')) installer = 'pnpm';
20
30
 
21
- if (args[0] === 'install' || args[0] === 'i') {
31
+ // 3. Installation Verbs
32
+ if (args[0] === 'install' || args[0] === 'i' || args[0] === 'add') {
22
33
  isInstallMode = true;
23
34
  target = args.find((a, i) => i > 0 && !a.startsWith('-')) || '.';
24
35
  }
25
36
 
26
37
  if (args.includes('--zift')) {
27
38
  isInstallMode = true;
28
- target = args.find(a => !a.startsWith('-') && !['install', 'i', 'npm'].includes(a)) || '.';
39
+ target = args.find(a => !a.startsWith('-') && !['install', 'i', 'add', 'npm', 'bun', 'pnpm'].includes(a)) || '.';
29
40
  }
30
41
 
42
+ // 4. Flags
31
43
  for (let i = 0; i < args.length; i++) {
32
44
  if (args[i] === '--format' && args[i + 1]) {
33
45
  format = args[i + 1];
@@ -35,26 +47,28 @@ async function main() {
35
47
  }
36
48
  }
37
49
 
50
+ // 5. No Args? Show Help
38
51
  if (args.length === 0) {
39
52
  showHelp();
40
53
  return;
41
54
  }
42
55
 
56
+ // 6. Execution
43
57
  const isLocal = fs.existsSync(target) && fs.lstatSync(target).isDirectory();
44
58
 
45
59
  if (isLocal) {
46
60
  await runLocalScan(target, format);
47
61
  } else {
48
- await runRemoteAudit(target, format, isInstallMode);
62
+ await runRemoteAudit(target, format, installer);
49
63
  }
50
64
  }
51
65
 
52
66
  async function runSetup() {
53
67
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
54
- console.log(chalk.blue.bold('\nšŸ›”ļø Zift Secure Alias Setup'));
55
- console.log(chalk.gray('Configure `npm install --zift` for automatic security audits.\n'));
68
+ console.log(chalk.blue.bold('\nšŸ›”ļø Zift Secure Alias Setup (Universal)'));
69
+ console.log(chalk.gray('Configure secure wrappers for npm, bun, and pnpm.\n'));
56
70
 
57
- const question = chalk.white('Add the Zift secure wrapper to your shell profile? (y/n): ');
71
+ const question = chalk.white('Add secure wrappers to your shell profile? (y/n): ');
58
72
 
59
73
  rl.question(question, (answer) => {
60
74
  rl.close();
@@ -66,74 +80,56 @@ async function runSetup() {
66
80
  } else {
67
81
  reloadCmd = setupUnix();
68
82
  }
69
- console.log(chalk.green('\nāœ… Setup complete! Profile updated.'));
70
- console.log(chalk.yellow.bold(`\nTo activate IMMEDIATELY, run this command:`));
71
- console.log(chalk.cyan.inverse(` ${reloadCmd} \n`));
72
- console.log(chalk.gray('Alternatively, simply restart your terminal.'));
83
+ console.log(chalk.green('\nāœ… Setup complete! All package managers are now secured.'));
84
+ console.log(chalk.yellow.bold(`\nTo activate IMMEDIATELY, run: `) + chalk.cyan.inverse(` ${reloadCmd} `));
73
85
  } catch (e) {
74
86
  console.error(chalk.red('\nāŒ Setup failed: ') + e.message);
75
87
  }
76
- } else {
77
- console.log(chalk.yellow('\nSetup cancelled.'));
78
88
  }
79
89
  });
80
90
  }
81
91
 
82
92
  function setupWindows() {
83
93
  const psFunction = `
84
- # Zift Secure Alias
85
- function npm {
86
- if ($args -contains "--zift") {
87
- $pkg = $args | Where-Object { $_ -ne "install" -and $_ -ne "i" -and $_ -ne "--zift" } | Select-Object -First 1
88
- Write-Host "\nšŸ›”ļø Zift: Intercepting installation for audit...\n" -ForegroundColor Green
89
- npx @7nsane/zift@latest install $pkg
90
- } else {
91
- & (Get-Command npm.cmd).Definition @args
92
- }
93
- }
94
+ # Zift Secure Wrappers
95
+ function npm { if ($args -contains "--zift") { npx @7nsane/zift@latest install ($args | Where-Object { $_ -ne "install" -and $_ -ne "i" -and $_ -ne "--zift" } | Select-Object -First 1) } else { & (Get-Command npm.cmd).Definition @args } }
96
+ function bun { if ($args -contains "--zift") { npx @7nsane/zift@latest install ($args | Where-Object { $_ -ne "add" -and $_ -ne "install" -and $_ -ne "--zift" } | Select-Object -First 1) --bun } else { & (Get-Command bun.exe).Definition @args } }
97
+ function pnpm { if ($args -contains "--zift") { npx @7nsane/zift@latest install ($args | Where-Object { $_ -ne "add" -and $_ -ne "install" -and $_ -ne "i" -and $_ -ne "--zift" } | Select-Object -First 1) --pnpm } else { & (Get-Command pnpm.cmd).Definition @args } }
94
98
  `;
95
99
  const profilePath = cp.execSync('powershell -NoProfile -Command "echo $PROFILE"').toString().trim();
96
- const profileDir = path.dirname(profilePath);
97
- if (!fs.existsSync(profileDir)) fs.mkdirSync(profileDir, { recursive: true });
98
100
  fs.appendFileSync(profilePath, psFunction);
99
101
  return '. $PROFILE';
100
102
  }
101
103
 
102
104
  function setupUnix() {
103
- const bashFunction = `
104
- # Zift Secure Alias
105
- npm() {
105
+ const wrapperPattern = (cmd, aliasCmd) => `
106
+ ${cmd}() {
106
107
  if [[ "$*" == *"--zift"* ]]; then
107
- pkg=$(echo "$@" | sed 's/install//g; s/ i //g; s/--zift//g' | xargs)
108
- npx @7nsane/zift@latest install $pkg
108
+ pkg=$(echo "$@" | sed 's/install//g; s/add//g; s/ i //g; s/--zift//g' | xargs)
109
+ npx @7nsane/zift@latest install $pkg --${cmd}
109
110
  else
110
- command npm "$@"
111
+ command ${cmd} "$@"
111
112
  fi
112
113
  }
113
114
  `;
115
+ const shellFunctions = wrapperPattern('npm') + wrapperPattern('bun') + wrapperPattern('pnpm');
114
116
  const home = os.homedir();
115
117
  const profiles = [path.join(home, '.bashrc'), path.join(home, '.zshrc')];
116
- let reloadTarget = '~/.zshrc';
117
- profiles.forEach(p => {
118
- if (fs.existsSync(p)) {
119
- fs.appendFileSync(p, bashFunction);
120
- if (p.endsWith('.bashrc')) reloadTarget = '~/.bashrc';
121
- }
122
- });
123
- return `source ${reloadTarget}`;
118
+ profiles.forEach(p => { if (fs.existsSync(p)) fs.appendFileSync(p, shellFunctions); });
119
+ return 'source ~/.zshrc # or ~/.bashrc';
124
120
  }
125
121
 
126
- async function runLocalScan(targetDir, format) {
127
- const scanner = new PackageScanner(targetDir);
128
- if (format === 'text') console.log(chalk.blue(`\nšŸ” Scanning local directory: ${path.resolve(targetDir)}`));
129
- try {
130
- const findings = await scanner.scan();
131
- handleFindings(findings, format, targetDir);
132
- } catch (err) { handleError(err, format); }
133
- }
122
+ async function runRemoteAudit(packageName, format, installer) {
123
+ if (format === 'text') console.log(chalk.blue(`\nšŸŒ Remote Audit [via ${installer}]: Pre-scanning '${packageName}'...`));
124
+
125
+ // Typosquat Check
126
+ const { checkTyposquat } = require('../src/utils/typo');
127
+ const typoMatch = checkTyposquat(packageName);
128
+ if (typoMatch && format === 'text') {
129
+ console.log(chalk.red.bold(`\nāš ļø TYPOSQUAT WARNING: '${packageName}' is very similar to '${typoMatch.target}'!`));
130
+ console.log(chalk.red(` If you meant '${typoMatch.target}', stop now.\n`));
131
+ }
134
132
 
135
- async function runRemoteAudit(packageName, format, installOnSuccess) {
136
- if (format === 'text') console.log(chalk.blue(`\nšŸŒ Remote Audit: Pre-scanning package '${packageName}'...`));
137
133
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'zift-audit-'));
138
134
  try {
139
135
  cp.execSync(`npm pack ${packageName}`, { cwd: tmpDir, stdio: 'ignore' });
@@ -144,50 +140,114 @@ async function runRemoteAudit(packageName, format, installOnSuccess) {
144
140
  const findings = await scanner.scan();
145
141
  handleFindings(findings, format, scanPath, true);
146
142
 
147
- if (format === 'text') {
148
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
149
- const promptText = findings.length > 0
150
- ? chalk.yellow(`\nāš ļø Suspicious patterns found. Still install '${packageName}'? (y/n): `)
151
- : chalk.blue(`\nAudit passed. Proceed with installation of '${packageName}'? (y/n): `);
152
-
153
- rl.question(promptText, (answer) => {
154
- rl.close();
155
- if (['y', 'yes'].includes(answer.toLowerCase())) {
156
- console.log(chalk.blue(`\nšŸ“¦ Installing ${packageName}...`));
157
- cp.execSync(`npm install ${packageName}`, { stdio: 'inherit' });
158
- console.log(chalk.green(`\nāœ… ${packageName} installed successfully.`));
159
- }
160
- cleanupAndExit(tmpDir, 0);
161
- });
162
- } else { cleanupAndExit(tmpDir, 0); }
143
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
144
+ const promptText = findings.length > 0
145
+ ? chalk.yellow(`\nāš ļø Suspicious patterns found. Still install '${packageName}' with ${installer}? (y/n): `)
146
+ : chalk.blue(`\nAudit passed. Proceed with installation of '${packageName}' via ${installer}? (y/n): `);
147
+
148
+ rl.question(promptText, (answer) => {
149
+ rl.close();
150
+ if (['y', 'yes'].includes(answer.toLowerCase())) {
151
+ console.log(chalk.blue(`\nšŸ“¦ Running '${installer} install ${packageName}'...`));
152
+ const installCmd = installer === 'bun' ? `bun add ${packageName}` : installer === 'pnpm' ? `pnpm add ${packageName}` : `npm install ${packageName}`;
153
+ try { cp.execSync(installCmd, { stdio: 'inherit' }); } catch (err) { }
154
+ }
155
+ cleanupAndExit(tmpDir, 0);
156
+ });
163
157
  } catch (err) { cleanupAndExit(tmpDir, 1); }
164
158
  }
165
159
 
166
- function handleFindings(findings, format, targetDir, skipExit = false) {
160
+ async function runLocalScan(target, format) {
161
+ if (format === 'text') console.log(chalk.blue(`\nšŸ” Scanning local directory: ${path.resolve(target)}`));
162
+ const scanner = new PackageScanner(target);
163
+ const results = await scanner.scan();
164
+
165
+ // Auditing lockfiles
166
+ const LockfileAuditor = require('../src/lockfile');
167
+ const auditor = new LockfileAuditor(target);
168
+ const lockfileFindings = auditor.audit();
169
+
170
+ handleFindings({ ...results, lockfileFindings }, format, target);
171
+ }
172
+
173
+ function handleFindings(data, format, targetDir, skipExit = false) {
174
+ const { results: findings, lifecycleScripts, lockfileFindings = [] } = data;
175
+
167
176
  if (format === 'json') {
168
- process.stdout.write(JSON.stringify({ target: targetDir, findings, summary: { Critical: findings.filter(f => f.classification === 'Critical').length, High: findings.filter(f => f.classification === 'High').length, Medium: findings.filter(f => f.classification === 'Medium').length, Low: findings.filter(f => f.classification === 'Low').length } }, null, 2));
177
+ process.stdout.write(JSON.stringify({
178
+ target: targetDir,
179
+ findings,
180
+ lifecycleScripts,
181
+ lockfileFindings,
182
+ summary: getSummary(findings)
183
+ }, null, 2));
169
184
  if (!skipExit) process.exit(findings.some(f => f.score >= 90) ? 1 : 0);
170
185
  return;
171
186
  }
187
+
188
+ // Lifecycle Summary
189
+ if (Object.keys(lifecycleScripts).length > 0) {
190
+ console.log(chalk.bold('\nšŸ“¦ Detected Lifecycle Scripts:'));
191
+ for (const [hook, cmd] of Object.entries(lifecycleScripts)) {
192
+ console.log(chalk.yellow(` - ${hook}: `) + chalk.white(cmd));
193
+ }
194
+ }
195
+
196
+ // Lockfile Summary
197
+ if (lockfileFindings.length > 0) {
198
+ console.log(chalk.bold('\nšŸ”’ Lockfile Security Audit:'));
199
+ lockfileFindings.forEach(f => {
200
+ const color = f.severity === 'High' ? chalk.red : chalk.yellow;
201
+ console.log(color(` - [${f.severity}] ${f.package}: ${f.type} (${f.source})`));
202
+ });
203
+ }
204
+
172
205
  if (findings.length === 0) {
173
- if (!skipExit) { console.log(chalk.green('\nāœ… No suspicious patterns detected. All modules safe.')); process.exit(0); }
206
+ if (!skipExit) {
207
+ console.log(chalk.green('\nāœ… No suspicious AST patterns detected.'));
208
+ process.exit(0);
209
+ }
174
210
  return;
175
211
  }
212
+
213
+ console.log(chalk.bold('\nšŸ” Behavioral AST Findings:'));
176
214
  findings.forEach(f => {
177
215
  const color = { 'Critical': chalk.red.bold, 'High': chalk.red, 'Medium': chalk.yellow, 'Low': chalk.blue }[f.classification];
178
216
  console.log(color(`[${f.classification}] ${f.id} ${f.name} (Score: ${f.score})`));
179
217
  f.triggers.forEach(t => console.log(chalk.white(` - ${t.type} in ${t.file}:${t.line} [${t.context}]`)));
180
218
  console.log('');
181
219
  });
182
- if (!skipExit) process.exit(findings[0].score >= 90 ? 1 : 0);
220
+
221
+ if (!skipExit) process.exit(findings.some(f => f.score >= 90) ? 1 : 0);
222
+ }
223
+
224
+ function runInit() {
225
+ const config = {
226
+ severity: {
227
+ critical: 90,
228
+ high: 70,
229
+ medium: 50
230
+ },
231
+ ignore: ['node_modules', '.git', 'test', 'dist'],
232
+ parallel: true,
233
+ cache: true
234
+ };
235
+ fs.writeFileSync(path.join(process.cwd(), '.zift.json'), JSON.stringify(config, null, 2));
236
+ fs.writeFileSync(path.join(process.cwd(), '.ziftignore'), '# Add patterns to ignore here\nnode_modules\ndist\ncoverage\n');
237
+ console.log(chalk.green('\nāœ… Initialized Zift configuration (.zift.json and .ziftignore)'));
183
238
  }
184
239
 
185
240
  function showHelp() {
186
- console.log(chalk.blue.bold('\nšŸ›”ļø Zift - The Elite Security Scanner\n'));
241
+ console.log(chalk.blue.bold('\nšŸ›”ļø Zift v2.0.0 - Intelligent Pre-install Security Gate\n'));
187
242
  console.log('Usage:');
188
- console.log(' zift setup Configure secure npm wrapper');
189
- console.log(' zift install <pkg> Audit and install package');
190
- console.log(' zift . Scan local directory');
243
+ console.log(' zift setup Secure npm, bun, and pnpm');
244
+ console.log(' zift init Initialize configuration');
245
+ console.log(' zift install <pkg> Scan and install package');
246
+ console.log(' zift . Scan current directory');
247
+ console.log('\nOptions:');
248
+ console.log(' --bun Use Bun for installation');
249
+ console.log(' --pnpm Use pnpm for installation');
250
+ console.log(' --format json Output as JSON');
191
251
  }
192
252
 
193
253
  function cleanupAndExit(dir, code) {
@@ -200,4 +260,16 @@ function handleError(err, format) {
200
260
  process.exit(1);
201
261
  }
202
262
 
263
+ function getSummary(findings) {
264
+ const s = { Critical: 0, High: 0, Medium: 0, Low: 0 };
265
+ findings.forEach(f => s[f.classification]++);
266
+ return s;
267
+ }
268
+
269
+ function printSummary(findings) {
270
+ const s = getSummary(findings);
271
+ console.log(chalk.bold('Severity Summary:'));
272
+ console.log(chalk.red(` Critical: ${s.Critical}\n High: ${s.High}`));
273
+ }
274
+
203
275
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7nsane/zift",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "A high-performance, deterministic security scanner for npm packages.",
5
5
  "main": "src/scanner.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  "zift-scanner": "bin/zift.js"
9
9
  },
10
10
  "scripts": {
11
- "test": "node v1_launch_pack/tests/verify.js",
11
+ "test": "echo \"Error: no test specified\" && exit 1",
12
12
  "scan": "node bin/zift"
13
13
  },
14
14
  "keywords": [
package/src/collector.js CHANGED
@@ -4,22 +4,25 @@ const { calculateEntropy } = require('./utils/entropy');
4
4
 
5
5
  class ASTCollector {
6
6
  constructor() {
7
- this.facts = {
7
+ this.entropyThreshold = 4.8;
8
+ this.maxFileSize = 512 * 1024;
9
+ this.maxStringLengthForEntropy = 2048;
10
+ }
11
+
12
+ collect(code, filePath) {
13
+ const facts = {
8
14
  ENV_READ: [],
9
15
  FILE_READ_SENSITIVE: [],
10
16
  NETWORK_SINK: [],
11
17
  DYNAMIC_EXECUTION: [],
12
18
  OBFUSCATION: [],
13
- FILE_WRITE_STARTUP: []
19
+ FILE_WRITE_STARTUP: [],
20
+ SHELL_EXECUTION: [],
21
+ ENCODER_USE: []
14
22
  };
15
- this.flows = [];
16
- this.entropyThreshold = 4.8;
17
- this.maxFileSize = 512 * 1024; // 512KB cap for static analysis
18
- this.maxStringLengthForEntropy = 2048; // Don't calculate entropy for massive blobs
19
- }
23
+ const flows = [];
24
+ const sourceCode = code;
20
25
 
21
- collect(code, filePath) {
22
- this.sourceCode = code;
23
26
  let ast;
24
27
  try {
25
28
  ast = acorn.parse(code, { ecmaVersion: 2020, sourceType: 'module', locations: true });
@@ -27,7 +30,7 @@ class ASTCollector {
27
30
  try {
28
31
  ast = acorn.parse(code, { ecmaVersion: 2020, sourceType: 'script', locations: true });
29
32
  } catch (error_) {
30
- return { facts: this.facts, flows: this.flows };
33
+ return { facts, flows };
31
34
  }
32
35
  }
33
36
 
@@ -36,7 +39,7 @@ class ASTCollector {
36
39
  if (typeof node.value === 'string' && node.value.length > 20 && node.value.length < this.maxStringLengthForEntropy) {
37
40
  const entropy = calculateEntropy(node.value);
38
41
  if (entropy > this.entropyThreshold) {
39
- this.facts.OBFUSCATION.push({
42
+ facts.OBFUSCATION.push({
40
43
  file: filePath,
41
44
  line: node.loc.start.line,
42
45
  reason: `High entropy string (${entropy.toFixed(2)})`,
@@ -45,45 +48,88 @@ class ASTCollector {
45
48
  }
46
49
  }
47
50
  },
48
- CallExpression: (node) => {
49
- const calleeCode = this.getSourceCode(node.callee);
51
+ CallExpression: (node, state, ancestors) => {
52
+ const calleeCode = sourceCode.substring(node.callee.start, node.callee.end);
50
53
 
51
- // Detect eval / Function
52
54
  if (calleeCode === 'eval' || calleeCode === 'Function') {
53
- this.facts.DYNAMIC_EXECUTION.push({
55
+ facts.DYNAMIC_EXECUTION.push({
54
56
  file: filePath,
55
57
  line: node.loc.start.line,
56
58
  type: calleeCode
57
59
  });
58
60
  }
59
61
 
60
- // Detect Sinks
62
+ if (calleeCode === 'require' && node.arguments.length > 0 && node.arguments[0].type !== 'Literal') {
63
+ facts.DYNAMIC_EXECUTION.push({
64
+ file: filePath,
65
+ line: node.loc.start.line,
66
+ type: 'dynamic_require',
67
+ variable: sourceCode.substring(node.arguments[0].start, node.arguments[0].end)
68
+ });
69
+ }
70
+
61
71
  if (this.isNetworkSink(calleeCode)) {
62
- this.facts.NETWORK_SINK.push({
72
+ facts.NETWORK_SINK.push({
63
73
  file: filePath,
64
74
  line: node.loc.start.line,
65
75
  callee: calleeCode
66
76
  });
67
77
  }
68
78
 
69
- // Detect Sources
70
- if (this.isSensitiveFileRead(calleeCode, node)) {
71
- this.facts.FILE_READ_SENSITIVE.push({
79
+ if (this.isShellSink(calleeCode)) {
80
+ facts.SHELL_EXECUTION.push({
72
81
  file: filePath,
73
82
  line: node.loc.start.line,
74
- path: node.arguments[0] ? this.getSourceCode(node.arguments[0]) : 'unknown'
83
+ callee: calleeCode
75
84
  });
76
85
  }
86
+
87
+ if (this.isEncoder(calleeCode)) {
88
+ facts.ENCODER_USE.push({
89
+ file: filePath,
90
+ line: node.loc.start.line,
91
+ type: calleeCode
92
+ });
93
+ }
94
+
95
+ if (this.isSensitiveFileRead(calleeCode, node, sourceCode)) {
96
+ facts.FILE_READ_SENSITIVE.push({
97
+ file: filePath,
98
+ line: node.loc.start.line,
99
+ path: node.arguments[0] ? sourceCode.substring(node.arguments[0].start, node.arguments[0].end) : 'unknown'
100
+ });
101
+ }
102
+
103
+ node.arguments.forEach((arg, index) => {
104
+ const argCode = sourceCode.substring(arg.start, arg.end);
105
+ // Improved check: Does the expression contain any variable we know is tainted?
106
+ const isArgTainted = argCode.includes('process.env') || flows.some(f => {
107
+ const regex = new RegExp(`\\b${f.toVar}\\b`);
108
+ return regex.test(argCode);
109
+ });
110
+
111
+ if (isArgTainted) {
112
+ const funcNode = this.findFunctionDefinition(calleeCode, ast);
113
+ if (funcNode && funcNode.params[index]) {
114
+ const paramName = funcNode.params[index].name;
115
+ flows.push({
116
+ fromVar: argCode,
117
+ toVar: `${calleeCode}:${paramName}`,
118
+ file: filePath,
119
+ line: node.loc.start.line
120
+ });
121
+ }
122
+ }
123
+ });
77
124
  },
78
125
  MemberExpression: (node) => {
79
- const objectCode = this.getSourceCode(node.object);
80
- // Detect process.env
126
+ const objectCode = sourceCode.substring(node.object.start, node.object.end);
81
127
  if (objectCode === 'process.env' || objectCode === 'process["env"]' || objectCode === "process['env']") {
82
128
  const property = node.property.name || (node.property.type === 'Literal' ? node.property.value : null);
83
129
  const whitelist = ['NODE_ENV', 'TIMING', 'DEBUG', 'VERBOSE', 'CI', 'APPDATA', 'HOME', 'USERPROFILE', 'PATH', 'PWD'];
84
- if (whitelist.includes(property)) return; // Whitelist common env check
130
+ if (whitelist.includes(property)) return;
85
131
 
86
- this.facts.ENV_READ.push({
132
+ facts.ENV_READ.push({
87
133
  file: filePath,
88
134
  line: node.loc.start.line,
89
135
  variable: property ? `process.env.${property}` : 'process.env'
@@ -92,37 +138,105 @@ class ASTCollector {
92
138
  },
93
139
  VariableDeclarator: (node) => {
94
140
  if (node.init && node.id.type === 'Identifier') {
95
- const from = this.getSourceCode(node.init);
96
- this.flows.push({
141
+ const from = sourceCode.substring(node.init.start, node.init.end);
142
+ flows.push({
97
143
  fromVar: from,
98
144
  toVar: node.id.name,
99
145
  file: filePath,
100
146
  line: node.loc.start.line
101
147
  });
102
148
  }
149
+ },
150
+ AssignmentExpression: (node) => {
151
+ if (node.left.type === 'MemberExpression' && node.right.type === 'Identifier') {
152
+ const from = sourceCode.substring(node.right.start, node.right.end);
153
+ const to = sourceCode.substring(node.left.start, node.left.end);
154
+ flows.push({
155
+ fromVar: from,
156
+ toVar: to,
157
+ file: filePath,
158
+ line: node.loc.start.line
159
+ });
160
+ }
161
+ },
162
+ ObjectExpression: (node, state, ancestors) => {
163
+ const parent = ancestors[ancestors.length - 2];
164
+ if (parent && parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
165
+ const objName = parent.id.name;
166
+ node.properties.forEach(prop => {
167
+ if (prop.value.type === 'MemberExpression' || prop.value.type === 'Identifier') {
168
+ const valCode = sourceCode.substring(prop.value.start, prop.value.end);
169
+ if (valCode.includes('process.env') || flows.some(f => f.toVar === valCode)) {
170
+ flows.push({
171
+ fromVar: valCode,
172
+ toVar: `${objName}.${sourceCode.substring(prop.key.start, prop.key.end)}`,
173
+ file: filePath,
174
+ line: prop.loc.start.line
175
+ });
176
+ }
177
+ }
178
+ });
179
+ }
103
180
  }
104
181
  });
105
182
 
106
- return { facts: this.facts, flows: this.flows };
183
+ return { facts, flows };
107
184
  }
108
185
 
109
186
  isNetworkSink(calleeCode) {
110
- const methodSinks = ['http.request', 'https.request', 'http.get', 'https.get', 'net.connect', 'dns.lookup', 'dns.resolve', 'fetch', 'axios'];
111
-
112
- // Match methods like http.request but avoid requestIdleCallback or local 'request' variables
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
113
193
  return methodSinks.some(sink => {
114
- return calleeCode === sink || calleeCode.endsWith('.' + sink);
194
+ if (calleeCode === sink) return true;
195
+ if (calleeCode.endsWith('.' + sink)) return true;
196
+ // Catch cases like require('https').get
197
+ if (sink.includes('.') && calleeCode.endsWith(sink.split('.')[1]) && calleeCode.includes(sink.split('.')[0])) return true;
198
+ return false;
115
199
  }) && !calleeCode.includes('IdleCallback');
116
200
  }
117
201
 
118
- isSensitiveFileRead(calleeCode, node) {
202
+ isShellSink(calleeCode) {
203
+ const shellSinks = ['child_process.exec', 'child_process.spawn', 'child_process.execSync', 'exec', 'spawn', 'execSync'];
204
+ return shellSinks.some(sink => {
205
+ if (calleeCode === sink) return true;
206
+ if (calleeCode.endsWith('.' + sink)) return true;
207
+ if (sink.includes('.') && calleeCode.endsWith(sink.split('.')[1]) && calleeCode.includes(sink.split('.')[0])) return true;
208
+ return false;
209
+ });
210
+ }
211
+
212
+ isEncoder(calleeCode) {
213
+ const encoders = ['Buffer.from', 'btoa', 'atob'];
214
+ return encoders.some(enc => calleeCode === enc || calleeCode.endsWith('.' + enc));
215
+ }
216
+
217
+ findFunctionDefinition(name, ast) {
218
+ let found = null;
219
+ walk.simple(ast, {
220
+ FunctionDeclaration: (node) => {
221
+ if (node.id.name === name) found = node;
222
+ },
223
+ VariableDeclarator: (node) => {
224
+ if (node.id.name === name && node.init && (node.init.type === 'ArrowFunctionExpression' || node.init.type === 'FunctionExpression')) {
225
+ found = node.init;
226
+ }
227
+ }
228
+ });
229
+ return found;
230
+ }
231
+
232
+ isSensitiveFileRead(calleeCode, node, sourceCode) {
119
233
  if (!calleeCode.includes('fs.readFile') && !calleeCode.includes('fs.readFileSync') &&
120
234
  !calleeCode.includes('fs.promises.readFile')) return false;
121
235
 
122
236
  if (node.arguments.length > 0 && node.arguments[0].type === 'Literal') {
123
- const path = String(node.arguments[0].value);
237
+ const pathValue = String(node.arguments[0].value);
124
238
  const sensitive = ['.ssh', '.env', 'shadow', 'passwd', 'credentials', 'token'];
125
- return sensitive.some((s) => path.toLowerCase().includes(s));
239
+ return sensitive.some((s) => pathValue.toLowerCase().includes(s));
126
240
  }
127
241
  return false;
128
242
  }
package/src/engine.js CHANGED
@@ -27,7 +27,15 @@ class SafetyEngine {
27
27
 
28
28
  // Check required facts
29
29
  for (const req of rule.requires) {
30
- const matchedFacts = facts[req] || [];
30
+ let matchedFacts = facts[req] || [];
31
+
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
+ }
38
+
31
39
  if (matchedFacts.length === 0) return null; // Rule not matched
32
40
  triggers.push(...matchedFacts.map(f => ({ ...f, type: req })));
33
41
  }
@@ -38,31 +46,37 @@ class SafetyEngine {
38
46
  const matchedOpts = facts[opt] || [];
39
47
  if (matchedOpts.length > 0) {
40
48
  triggers.push(...matchedOpts.map(f => ({ ...f, type: opt })));
41
- baseScore += 20; // Bonus for optional matches (e.g., obfuscation)
49
+ baseScore += 20;
42
50
  }
43
51
  }
44
52
  }
45
53
 
46
- // Apply Lifecycle Multiplier (1.8x)
54
+ // Apply Lifecycle Multiplier (2.0x for V2)
47
55
  const isInLifecycle = triggers.some(t => lifecycleFiles.has(t.file));
48
56
  if (isInLifecycle) {
49
- multiplier = 1.8;
57
+ multiplier = 2.0;
58
+ }
59
+
60
+ // Encoder Multiplier (1.5x)
61
+ const hasEncoder = facts['ENCODER_USE'] && facts['ENCODER_USE'].length > 0;
62
+ if (hasEncoder) {
63
+ multiplier *= 1.5;
50
64
  }
51
65
 
52
66
  // Cluster Bonus: Source + Sink
53
67
  const hasSource = triggers.some(t => t.type.includes('READ'));
54
- const hasSink = triggers.some(t => t.type.includes('SINK') || t.type === 'DYNAMIC_EXECUTION');
68
+ const hasSink = triggers.some(t => t.type.includes('SINK') || t.type === 'DYNAMIC_EXECUTION' || t.type === 'SHELL_EXECUTION');
55
69
  if (hasSource && hasSink) {
56
70
  baseScore += 40;
57
71
  }
58
72
 
59
73
  let finalScore = baseScore * multiplier;
60
74
 
61
- // Lifecycle Guard: ENV_READ + NETWORK_SINK + lifecycleContext = min 85 (High)
75
+ // Severe Cluster: ENV_READ + (NETWORK_SINK | SHELL_EXECUTION) + lifecycleContext = Critical (100)
62
76
  const isEnvRead = triggers.some(t => t.type === 'ENV_READ');
63
- const isNetworkSink = triggers.some(t => t.type === 'NETWORK_SINK');
64
- if (isEnvRead && isNetworkSink && isInLifecycle && finalScore < 85) {
65
- finalScore = 85;
77
+ const isDangerousSink = triggers.some(t => t.type === 'NETWORK_SINK' || t.type === 'SHELL_EXECUTION');
78
+ if (isEnvRead && isDangerousSink && isInLifecycle) {
79
+ finalScore = 100;
66
80
  }
67
81
 
68
82
  return {
package/src/lifecycle.js CHANGED
@@ -5,11 +5,12 @@ class LifecycleResolver {
5
5
  constructor(packageDir) {
6
6
  this.packageDir = packageDir;
7
7
  this.lifecycleFiles = new Set();
8
+ this.detectedScripts = {};
8
9
  }
9
10
 
10
11
  resolve() {
11
12
  const packageJsonPath = path.join(this.packageDir, 'package.json');
12
- if (!fs.existsSync(packageJsonPath)) return this.lifecycleFiles;
13
+ if (!fs.existsSync(packageJsonPath)) return { files: this.lifecycleFiles, scripts: this.detectedScripts };
13
14
 
14
15
  try {
15
16
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
@@ -19,6 +20,7 @@ class LifecycleResolver {
19
20
 
20
21
  for (const hook of lifecycleHooks) {
21
22
  if (scripts[hook]) {
23
+ this.detectedScripts[hook] = scripts[hook];
22
24
  this.extractFilesFromScript(scripts[hook]);
23
25
  }
24
26
  }
@@ -26,7 +28,7 @@ class LifecycleResolver {
26
28
  // Ignore parse errors
27
29
  }
28
30
 
29
- return this.lifecycleFiles;
31
+ return { files: this.lifecycleFiles, scripts: this.detectedScripts };
30
32
  }
31
33
 
32
34
  extractFilesFromScript(script) {
@@ -0,0 +1,99 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ class LockfileAuditor {
5
+ constructor(packageDir) {
6
+ this.packageDir = packageDir;
7
+ this.findings = [];
8
+ }
9
+
10
+ audit() {
11
+ const lockfiles = [
12
+ { name: 'package-lock.json', type: 'npm' },
13
+ { name: 'pnpm-lock.yaml', type: 'pnpm' },
14
+ { name: 'bun.lockb', type: 'bun' }
15
+ ];
16
+
17
+ for (const lock of lockfiles) {
18
+ const fullPath = path.join(this.packageDir, lock.name);
19
+ if (fs.existsSync(fullPath)) {
20
+ this.auditLockfile(fullPath, lock.type);
21
+ }
22
+ }
23
+
24
+ return this.findings;
25
+ }
26
+
27
+ auditLockfile(filePath, type) {
28
+ const content = fs.readFileSync(filePath, 'utf8');
29
+
30
+ if (type === 'npm') {
31
+ try {
32
+ const lock = JSON.parse(content);
33
+ this.checkNpmDependencies(lock.dependencies || lock.packages || {});
34
+ } catch (e) { }
35
+ } else if (type === 'pnpm') {
36
+ // pnpm-lock.yaml regex-based scanning (to avoid heavy yaml parser)
37
+ this.scanTextForUntrustedSources(content, 'pnpm');
38
+ } else if (type === 'bun') {
39
+ // bun.lockb is binary, but often contains readable URLs or has a text counterpart
40
+ this.scanTextForUntrustedSources(content, 'bun');
41
+ }
42
+ }
43
+
44
+ scanTextForUntrustedSources(content, type) {
45
+ // Look for git+ssh, git+https, github:, or non-npm https urls
46
+ const lines = content.split('\n');
47
+ lines.forEach((line, index) => {
48
+ const gitMatch = line.match(/(git\+ssh|git\+https|github:|[a-zA-Z0-9.\-_]+\/[a-zA-Z0-9.\-_]+#[a-f0-9]+)/);
49
+ if (gitMatch) {
50
+ this.findings.push({
51
+ type: 'UNTRUSTED_GIT_SOURCE',
52
+ package: `Line ${index + 1}`,
53
+ source: gitMatch[0],
54
+ severity: 'Medium'
55
+ });
56
+ }
57
+
58
+ const httpMatch = line.match(/https?:\/\/(?!(registry\.npmjs\.org|registry\.yarnpkg\.com))[a-zA-Z0-9.\-/_]+/);
59
+ if (httpMatch) {
60
+ this.findings.push({
61
+ type: 'NON_STANDARD_REGISTRY',
62
+ package: `Line ${index + 1}`,
63
+ source: httpMatch[0],
64
+ severity: 'High'
65
+ });
66
+ }
67
+ });
68
+ }
69
+
70
+ checkNpmDependencies(deps) {
71
+ for (const [name, info] of Object.entries(deps)) {
72
+ if (!name) continue;
73
+
74
+ const resolved = info.resolved || (info.version ? info.version : '');
75
+
76
+ // Detect Git Dependencies
77
+ if (resolved.includes('git+') || resolved.includes('github:')) {
78
+ this.findings.push({
79
+ type: 'UNTRUSTED_GIT_SOURCE',
80
+ package: name,
81
+ source: resolved,
82
+ severity: 'Medium'
83
+ });
84
+ }
85
+
86
+ // Detect HTTP based installs
87
+ if (resolved.startsWith('http:') || (resolved.startsWith('https:') && !resolved.includes('registry.npmjs.org'))) {
88
+ this.findings.push({
89
+ type: 'NON_STANDARD_REGISTRY',
90
+ package: name,
91
+ source: resolved,
92
+ severity: 'High'
93
+ });
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ module.exports = LockfileAuditor;
@@ -31,13 +31,30 @@ const RULES = [
31
31
  requires: ['OBFUSCATION', 'DYNAMIC_EXECUTION'],
32
32
  baseScore: 40,
33
33
  description: 'Detection of high-entropy strings being executed via eval or Function constructor.'
34
+ },
35
+ {
36
+ id: 'ZFT-005',
37
+ alias: 'SHELL_COMMAND_EXECUTION',
38
+ name: 'Shell Command Execution',
39
+ requires: ['SHELL_EXECUTION'],
40
+ optional: ['ENV_READ', 'FILE_READ_SENSITIVE'],
41
+ baseScore: 50,
42
+ description: 'Detection of shell command execution (child_process).'
43
+ },
44
+ {
45
+ id: 'ZFT-006',
46
+ alias: 'DYNAMIC_REQUIRE_DEPENDENCY',
47
+ name: 'Dynamic Require Dependency',
48
+ requires: ['DYNAMIC_EXECUTION'], // Will check if type === 'dynamic_require' in engine
49
+ baseScore: 30,
50
+ description: 'Detection of dynamic require calls where the dependency name is a variable.'
34
51
  }
35
52
  ];
36
53
 
37
54
  const CATEGORIES = {
38
55
  SOURCES: ['ENV_READ', 'FILE_READ_SENSITIVE'],
39
- SINKS: ['NETWORK_SINK', 'DYNAMIC_EXECUTION'],
40
- OBFUSCATION: ['OBFUSCATION'],
56
+ SINKS: ['NETWORK_SINK', 'DYNAMIC_EXECUTION', 'SHELL_EXECUTION'],
57
+ SIGNALS: ['OBFUSCATION', 'ENCODER_USE'],
41
58
  PERSISTENCE: ['FILE_WRITE_STARTUP']
42
59
  };
43
60
 
package/src/scanner.js CHANGED
@@ -3,6 +3,7 @@ const path = require('node:path');
3
3
  const ASTCollector = require('./collector');
4
4
  const LifecycleResolver = require('./lifecycle');
5
5
  const SafetyEngine = require('./engine');
6
+ const { getHash } = require('./utils/hash');
6
7
 
7
8
  class PackageScanner {
8
9
  constructor(packageDir) {
@@ -13,8 +14,15 @@ class PackageScanner {
13
14
  }
14
15
 
15
16
  async scan() {
16
- const lifecycleFiles = this.lifecycleResolver.resolve();
17
+ const { files: lifecycleFiles, scripts } = this.lifecycleResolver.resolve();
17
18
  const files = await this.getFiles();
19
+ this.detectedLifecycleScripts = scripts; // Store for formatter
20
+
21
+ // Initialize cache directory
22
+ const cacheDir = path.join(this.packageDir, 'node_modules', '.zift-cache');
23
+ if (!fs.existsSync(cacheDir)) {
24
+ try { fs.mkdirSync(cacheDir, { recursive: true }); } catch (e) { }
25
+ }
18
26
 
19
27
  let allFacts = {
20
28
  facts: {
@@ -23,29 +31,60 @@ class PackageScanner {
23
31
  NETWORK_SINK: [],
24
32
  DYNAMIC_EXECUTION: [],
25
33
  OBFUSCATION: [],
26
- FILE_WRITE_STARTUP: []
34
+ FILE_WRITE_STARTUP: [],
35
+ SHELL_EXECUTION: [],
36
+ ENCODER_USE: []
27
37
  },
28
38
  flows: []
29
39
  };
30
40
 
31
- for (const file of files) {
32
- const relativePath = path.relative(this.packageDir, file);
41
+ const pkgVersion = require('../package.json').version;
33
42
 
34
- // Refined ignore logic
35
- const ignorePatterns = ['node_modules', '.git', 'test', 'dist', 'coverage', 'docs', '.github'];
36
- if (ignorePatterns.some(p => relativePath.includes(p)) || relativePath.startsWith('.')) continue;
43
+ // Parallel processing with limited concurrency (8 files at a time)
44
+ const concurrency = 8;
45
+ for (let i = 0; i < files.length; i += concurrency) {
46
+ const chunk = files.slice(i, i + concurrency);
47
+ await Promise.all(chunk.map(async (file) => {
48
+ const stats = fs.statSync(file);
49
+ if (stats.size > 512 * 1024) return;
37
50
 
38
- const stats = fs.statSync(file);
39
- if (stats.size > 512 * 1024) continue; // Skip files > 512KB
51
+ const code = fs.readFileSync(file, 'utf8');
52
+ const fileHash = getHash(code + pkgVersion);
53
+ const cachePath = path.join(cacheDir, fileHash + '.json');
40
54
 
41
- const code = fs.readFileSync(file, 'utf8');
42
- const { facts, flows } = this.collector.collect(code, file);
55
+ let facts = {}, flows = [];
43
56
 
44
- // Merge facts
45
- for (const category in facts) {
46
- allFacts.facts[category].push(...facts[category]);
47
- }
48
- allFacts.flows.push(...flows);
57
+ if (fs.existsSync(cachePath)) {
58
+ // Cache hit: Load metadata
59
+ try {
60
+ const cached = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
61
+ facts = cached.facts || {};
62
+ flows = cached.flows || [];
63
+ } catch (e) {
64
+ // Corrupt cache: re-scan
65
+ const result = this.collector.collect(code, file);
66
+ facts = result.facts;
67
+ flows = result.flows;
68
+ }
69
+ } else {
70
+ // Cache miss: Scan and save
71
+ const result = this.collector.collect(code, file);
72
+ facts = result.facts;
73
+ flows = result.flows;
74
+
75
+ try {
76
+ fs.writeFileSync(cachePath, JSON.stringify({ facts, flows }));
77
+ } catch (e) { }
78
+ }
79
+
80
+ // Merge facts (Synchronized)
81
+ for (const category in facts) {
82
+ if (allFacts.facts[category]) {
83
+ allFacts.facts[category].push(...facts[category]);
84
+ }
85
+ }
86
+ allFacts.flows.push(...flows);
87
+ }));
49
88
  }
50
89
 
51
90
  const findings = this.engine.evaluate(allFacts, lifecycleFiles);
@@ -53,17 +92,28 @@ class PackageScanner {
53
92
  }
54
93
 
55
94
  async getFiles() {
95
+ // Load .ziftignore
96
+ const ziftIgnorePath = path.join(this.packageDir, '.ziftignore');
97
+ let ignoreLines = ['node_modules', '.git', 'dist', 'build', 'coverage', 'test', 'tests'];
98
+ if (fs.existsSync(ziftIgnorePath)) {
99
+ const content = fs.readFileSync(ziftIgnorePath, 'utf8');
100
+ ignoreLines = [...ignoreLines, ...content.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'))];
101
+ }
102
+
56
103
  const getJsFiles = (dir) => {
57
104
  const results = [];
58
105
  const list = fs.readdirSync(dir);
59
106
  for (const file of list) {
60
107
  const fullPath = path.join(dir, file);
108
+ const relativePath = path.relative(this.packageDir, fullPath);
109
+
110
+ // Simple ignore check
111
+ if (ignoreLines.some(pattern => relativePath.includes(pattern) || file === pattern)) continue;
112
+ if (file.startsWith('.') && file !== '.ziftignore') continue;
113
+
61
114
  const stat = fs.statSync(fullPath);
62
115
  if (stat && stat.isDirectory()) {
63
- const ignoreDirs = ['node_modules', '.git', 'dist', 'build', 'coverage', 'test', 'tests'];
64
- if (!ignoreDirs.includes(file) && !file.startsWith('.')) {
65
- results.push(...getJsFiles(fullPath));
66
- }
116
+ results.push(...getJsFiles(fullPath));
67
117
  } else if (file.endsWith('.js')) {
68
118
  results.push(fullPath);
69
119
  }
@@ -76,23 +126,26 @@ class PackageScanner {
76
126
  formatFindings(findings) {
77
127
  const sorted = findings.sort((a, b) => b.score - a.score);
78
128
 
79
- return sorted.map(f => {
80
- let classification = 'Low';
81
- if (f.score >= 90) classification = 'Critical';
82
- else if (f.score >= 70) classification = 'High';
83
- else if (f.score >= 50) classification = 'Medium';
84
-
85
- return {
86
- ...f,
87
- classification,
88
- triggers: f.triggers.map(t => ({
89
- type: t.type,
90
- file: path.relative(this.packageDir, t.file),
91
- line: t.line,
92
- context: t.reason || t.callee || t.variable || t.path
93
- }))
94
- };
95
- });
129
+ return {
130
+ results: sorted.map(f => {
131
+ let classification = 'Low';
132
+ if (f.score >= 90) classification = 'Critical';
133
+ else if (f.score >= 70) classification = 'High';
134
+ else if (f.score >= 50) classification = 'Medium';
135
+
136
+ return {
137
+ ...f,
138
+ classification,
139
+ triggers: f.triggers.map(t => ({
140
+ type: t.type,
141
+ file: path.relative(this.packageDir, t.file),
142
+ line: t.line,
143
+ context: t.reason || t.callee || t.variable || t.path
144
+ }))
145
+ };
146
+ }),
147
+ lifecycleScripts: this.detectedLifecycleScripts
148
+ };
96
149
  }
97
150
  }
98
151
 
@@ -0,0 +1,7 @@
1
+ const crypto = require('node:crypto');
2
+
3
+ function getHash(data) {
4
+ return crypto.createHash('sha256').update(data).digest('hex');
5
+ }
6
+
7
+ module.exports = { getHash };
@@ -0,0 +1,43 @@
1
+ function levenshtein(s1, s2) {
2
+ const scores = [];
3
+ for (let i = 0; i <= s1.length; i++) {
4
+ let lastValue = i;
5
+ for (let j = 0; j <= s2.length; j++) {
6
+ if (i === 0) {
7
+ scores[j] = j;
8
+ } else if (j > 0) {
9
+ let newValue = scores[j - 1];
10
+ if (s1.charAt(i - 1) !== s2.charAt(j - 1)) {
11
+ newValue = Math.min(Math.min(newValue, lastValue), scores[j]) + 1;
12
+ }
13
+ scores[j - 1] = lastValue;
14
+ lastValue = newValue;
15
+ }
16
+ }
17
+ if (i > 0) scores[s2.length] = lastValue;
18
+ }
19
+ return scores[s2.length];
20
+ }
21
+
22
+ const TOP_PACKAGES = [
23
+ 'react', 'vue', 'axios', 'express', 'lodash', 'moment', 'next', 'react-dom',
24
+ 'chalk', 'commander', 'fs-extra', 'glob', 'inquirer', 'jest', 'request',
25
+ 'typescript', 'webpack', 'babel-core', 'eslint', 'prettier'
26
+ ];
27
+
28
+ function checkTyposquat(name) {
29
+ if (!name || TOP_PACKAGES.includes(name)) return null;
30
+
31
+ for (const top of TOP_PACKAGES) {
32
+ const distance = levenshtein(name, top);
33
+ if (distance === 1 || (distance === 2 && top.length >= 5)) {
34
+ return {
35
+ target: top,
36
+ distance: distance
37
+ };
38
+ }
39
+ }
40
+ return null;
41
+ }
42
+
43
+ module.exports = { checkTyposquat };
Binary file