@deepv-code/safe-npm 0.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/LICENSE +21 -0
- package/README.md +120 -0
- package/README.zh-CN.md +121 -0
- package/dist/cli/args-parser.d.ts +11 -0
- package/dist/cli/args-parser.js +36 -0
- package/dist/cli/check.d.ts +5 -0
- package/dist/cli/check.js +126 -0
- package/dist/cli/proxy.d.ts +1 -0
- package/dist/cli/proxy.js +4 -0
- package/dist/data/popular-packages.d.ts +9 -0
- package/dist/data/popular-packages.js +83 -0
- package/dist/i18n/en.d.ts +43 -0
- package/dist/i18n/en.js +50 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/index.js +11 -0
- package/dist/i18n/zh.d.ts +2 -0
- package/dist/i18n/zh.js +50 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +77 -0
- package/dist/scanner/code-analyzer.d.ts +2 -0
- package/dist/scanner/code-analyzer.js +130 -0
- package/dist/scanner/index.d.ts +3 -0
- package/dist/scanner/index.js +163 -0
- package/dist/scanner/patterns/exfiltration.d.ts +7 -0
- package/dist/scanner/patterns/exfiltration.js +49 -0
- package/dist/scanner/patterns/miner.d.ts +5 -0
- package/dist/scanner/patterns/miner.js +32 -0
- package/dist/scanner/patterns/obfuscation.d.ts +15 -0
- package/dist/scanner/patterns/obfuscation.js +110 -0
- package/dist/scanner/types.d.ts +26 -0
- package/dist/scanner/types.js +1 -0
- package/dist/scanner/typosquatting.d.ts +3 -0
- package/dist/scanner/typosquatting.js +126 -0
- package/dist/scanner/virustotal.d.ts +7 -0
- package/dist/scanner/virustotal.js +249 -0
- package/dist/scanner/vulnerability.d.ts +2 -0
- package/dist/scanner/vulnerability.js +42 -0
- package/dist/tui/App.d.ts +2 -0
- package/dist/tui/App.js +67 -0
- package/dist/tui/index.d.ts +1 -0
- package/dist/tui/index.js +6 -0
- package/dist/tui/screens/CheckScreen.d.ts +7 -0
- package/dist/tui/screens/CheckScreen.js +92 -0
- package/dist/tui/screens/PopularScreen.d.ts +7 -0
- package/dist/tui/screens/PopularScreen.js +39 -0
- package/dist/tui/screens/SettingsScreen.d.ts +6 -0
- package/dist/tui/screens/SettingsScreen.js +64 -0
- package/dist/utils/config.d.ts +16 -0
- package/dist/utils/config.js +69 -0
- package/dist/utils/npm-package.d.ts +38 -0
- package/dist/utils/npm-package.js +191 -0
- package/dist/utils/npm-runner.d.ts +7 -0
- package/dist/utils/npm-runner.js +56 -0
- package/package.json +48 -0
package/dist/i18n/zh.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const zh = {
|
|
2
|
+
// General
|
|
3
|
+
appName: 'safe-npm',
|
|
4
|
+
version: '版本',
|
|
5
|
+
// Risk levels
|
|
6
|
+
riskFatal: '致命',
|
|
7
|
+
riskHigh: '高危',
|
|
8
|
+
riskWarning: '警告',
|
|
9
|
+
riskSafe: '安全',
|
|
10
|
+
// Detection messages
|
|
11
|
+
virusDetected: '检测到已知恶意软件',
|
|
12
|
+
typosquatDetected: '检测到仿冒包 - 这可能是假冒包',
|
|
13
|
+
suspiciousCode: '检测到可疑代码模式',
|
|
14
|
+
cveFound: '发现已知漏洞',
|
|
15
|
+
minerDetected: '检测到挖矿脚本特征',
|
|
16
|
+
obfuscatedCode: '检测到代码混淆 - 可能隐藏恶意行为',
|
|
17
|
+
dangerousScript: '检测到危险的安装脚本',
|
|
18
|
+
blacklisted: '此包在已知恶意包名单中',
|
|
19
|
+
// Actions
|
|
20
|
+
blocked: '已阻止安装',
|
|
21
|
+
useForce: '如确认安全,可使用 --force 强制安装',
|
|
22
|
+
cannotBypass: '此威胁无法绕过',
|
|
23
|
+
continuing: '继续安装...',
|
|
24
|
+
// Scanner
|
|
25
|
+
scanning: '正在扫描包',
|
|
26
|
+
scanComplete: '扫描完成',
|
|
27
|
+
downloadingPackage: '正在下载包进行分析',
|
|
28
|
+
analyzingCode: '正在分析代码模式',
|
|
29
|
+
checkingVirustotal: '正在检查 VirusTotal 数据库',
|
|
30
|
+
checkingBlacklist: '正在检查已知恶意包名单',
|
|
31
|
+
// Errors
|
|
32
|
+
networkError: '网络错误 - 回退到本地缓存',
|
|
33
|
+
offlineMode: '离线模式运行中',
|
|
34
|
+
vtNoApiKey: '未配置 VirusTotal API 密钥 - 跳过在线扫描',
|
|
35
|
+
// TUI
|
|
36
|
+
tuiWelcome: '欢迎使用 safe-npm',
|
|
37
|
+
tuiPopular: '热门包',
|
|
38
|
+
tuiCheck: '检测包',
|
|
39
|
+
tuiSettings: '设置',
|
|
40
|
+
tuiQuit: '退出',
|
|
41
|
+
tuiLanguage: '语言',
|
|
42
|
+
// Suggestions
|
|
43
|
+
suggestionTitle: '💡 建议:您是否想安装官方正版包',
|
|
44
|
+
suggestionPrompt: '您是否要安装',
|
|
45
|
+
installingCorrect: '🚀 正在安装正确的包',
|
|
46
|
+
packageNotFound: '在注册表中未找到此包',
|
|
47
|
+
didYouMean: '您是指',
|
|
48
|
+
checkName: '请检查包名',
|
|
49
|
+
registryCheckFail: '包不存在',
|
|
50
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from './cli/args-parser.js';
|
|
3
|
+
import { proxyToNpm } from './cli/proxy.js';
|
|
4
|
+
import { checkPackages } from './cli/check.js';
|
|
5
|
+
import { startTui } from './tui/index.js';
|
|
6
|
+
import { hasConfigFile, saveConfig } from './utils/config.js';
|
|
7
|
+
import * as readline from 'readline';
|
|
8
|
+
async function promptLanguage() {
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const rl = readline.createInterface({
|
|
11
|
+
input: process.stdin,
|
|
12
|
+
output: process.stdout,
|
|
13
|
+
});
|
|
14
|
+
console.log('\nWelcome to safe-npm! / 欢迎使用 safe-npm!');
|
|
15
|
+
console.log('Please select your language / 请选择语言:');
|
|
16
|
+
console.log('1. English');
|
|
17
|
+
console.log('2. 中文 (Chinese)');
|
|
18
|
+
rl.question('Select [1/2] (default: 2): ', (answer) => {
|
|
19
|
+
if (answer.trim() === '1') {
|
|
20
|
+
saveConfig({ language: 'en' });
|
|
21
|
+
console.log('Language set to English.\n');
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
// Default to Chinese as per request context or generic 'other'
|
|
25
|
+
saveConfig({ language: 'zh' });
|
|
26
|
+
console.log('语言已设置为中文。\n');
|
|
27
|
+
}
|
|
28
|
+
rl.close();
|
|
29
|
+
resolve();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async function main() {
|
|
34
|
+
const args = process.argv.slice(2);
|
|
35
|
+
// First run check
|
|
36
|
+
if (!hasConfigFile()) {
|
|
37
|
+
await promptLanguage();
|
|
38
|
+
}
|
|
39
|
+
if (args.length === 0) {
|
|
40
|
+
// Show help or start TUI
|
|
41
|
+
console.log('safe-npm - A security-focused npm wrapper');
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log('Usage:');
|
|
44
|
+
console.log(' safe-npm <npm-command> Proxy npm commands with security checks');
|
|
45
|
+
console.log(' safe-npm check <pkg> Check package without installing');
|
|
46
|
+
console.log(' safe-npm tui Open interactive TUI');
|
|
47
|
+
console.log('');
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
const parsed = parseArgs(args);
|
|
51
|
+
if (parsed.isTui) {
|
|
52
|
+
startTui();
|
|
53
|
+
// Don't exit - Ink handles the process
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (parsed.isCheck) {
|
|
57
|
+
await checkPackages(parsed.packages);
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
if (parsed.isInstall && parsed.packages.length > 0) {
|
|
61
|
+
// Security scan before install
|
|
62
|
+
const safe = await checkPackages(parsed.packages, {
|
|
63
|
+
isForce: parsed.isForce,
|
|
64
|
+
install: true,
|
|
65
|
+
});
|
|
66
|
+
if (!safe) {
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Proxy to npm
|
|
71
|
+
const exitCode = await proxyToNpm(args);
|
|
72
|
+
process.exit(exitCode);
|
|
73
|
+
}
|
|
74
|
+
main().catch((err) => {
|
|
75
|
+
console.error('Error:', err.message);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { checkMinerPatterns } from './patterns/miner.js';
|
|
2
|
+
import { checkExfiltrationPatterns } from './patterns/exfiltration.js';
|
|
3
|
+
import { checkObfuscationPatterns } from './patterns/obfuscation.js';
|
|
4
|
+
import { t } from '../i18n/index.js';
|
|
5
|
+
import { extractAndScanPackage, getPackageInfo } from '../utils/npm-package.js';
|
|
6
|
+
// Dangerous postinstall script patterns
|
|
7
|
+
const dangerousScriptPatterns = [
|
|
8
|
+
// Direct command execution
|
|
9
|
+
/curl\s+.*\|\s*(bash|sh)/i,
|
|
10
|
+
/wget\s+.*\|\s*(bash|sh)/i,
|
|
11
|
+
/powershell\s+-.*downloadstring/i,
|
|
12
|
+
/powershell\s+.*iex/i,
|
|
13
|
+
// Reverse shells
|
|
14
|
+
/bash\s+-i\s+>&\s*\/dev\/tcp/i,
|
|
15
|
+
/nc\s+-e\s+\/bin\/(ba)?sh/i,
|
|
16
|
+
/python.*socket.*connect/i,
|
|
17
|
+
// Suspicious node execution
|
|
18
|
+
/node\s+-e\s+['"].*require.*child_process/i,
|
|
19
|
+
/node\s+-e\s+['"].*eval\s*\(/i,
|
|
20
|
+
// Environment variable exfiltration
|
|
21
|
+
/curl.*\$\{?npm_config/i,
|
|
22
|
+
/curl.*process\.env/i,
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Scan package.json scripts for dangerous patterns
|
|
26
|
+
*/
|
|
27
|
+
function scanScripts(scripts) {
|
|
28
|
+
const issues = [];
|
|
29
|
+
const dangerousScriptNames = ['preinstall', 'install', 'postinstall', 'preuninstall', 'postuninstall'];
|
|
30
|
+
for (const [name, script] of Object.entries(scripts)) {
|
|
31
|
+
// Check if it's a lifecycle script
|
|
32
|
+
const isLifecycleScript = dangerousScriptNames.includes(name);
|
|
33
|
+
for (const pattern of dangerousScriptPatterns) {
|
|
34
|
+
if (pattern.test(script)) {
|
|
35
|
+
issues.push({
|
|
36
|
+
type: 'suspicious_code',
|
|
37
|
+
severity: isLifecycleScript ? 'high' : 'warning',
|
|
38
|
+
message: t('suspiciousCode'),
|
|
39
|
+
details: `Dangerous pattern in "${name}" script: ${pattern.source}`,
|
|
40
|
+
});
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Check for obfuscated commands in scripts
|
|
45
|
+
if (isLifecycleScript && script.length > 500) {
|
|
46
|
+
// Very long script might be hiding something
|
|
47
|
+
issues.push({
|
|
48
|
+
type: 'suspicious_code',
|
|
49
|
+
severity: 'warning',
|
|
50
|
+
message: t('suspiciousCode'),
|
|
51
|
+
details: `Unusually long "${name}" script (${script.length} chars) - manual review recommended`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return issues;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Scan JavaScript file content for malicious patterns
|
|
59
|
+
*/
|
|
60
|
+
function scanFileContent(filePath, content) {
|
|
61
|
+
const issues = [];
|
|
62
|
+
// Check for miner patterns
|
|
63
|
+
const minerResult = checkMinerPatterns(content);
|
|
64
|
+
if (minerResult.matched) {
|
|
65
|
+
issues.push({
|
|
66
|
+
type: 'miner',
|
|
67
|
+
severity: 'high',
|
|
68
|
+
message: t('minerDetected'),
|
|
69
|
+
details: `Pattern matched in ${filePath}: ${minerResult.pattern}`,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// Check for data exfiltration patterns
|
|
73
|
+
const exfilFindings = checkExfiltrationPatterns(content);
|
|
74
|
+
for (const finding of exfilFindings) {
|
|
75
|
+
issues.push({
|
|
76
|
+
type: 'suspicious_code',
|
|
77
|
+
severity: 'high',
|
|
78
|
+
message: t('suspiciousCode'),
|
|
79
|
+
details: `${finding} (in ${filePath})`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Check for obfuscation patterns
|
|
83
|
+
const obfuscationResult = checkObfuscationPatterns(content);
|
|
84
|
+
if (obfuscationResult.matched) {
|
|
85
|
+
issues.push({
|
|
86
|
+
type: 'suspicious_code',
|
|
87
|
+
severity: 'warning',
|
|
88
|
+
message: t('suspiciousCode'),
|
|
89
|
+
details: `Code obfuscation detected in ${filePath}: ${obfuscationResult.pattern}`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return issues;
|
|
93
|
+
}
|
|
94
|
+
export async function scanCodePatterns(packageName) {
|
|
95
|
+
const issues = [];
|
|
96
|
+
// First, try to get basic package info without downloading
|
|
97
|
+
const packageInfo = await getPackageInfo(packageName);
|
|
98
|
+
if (!packageInfo) {
|
|
99
|
+
// Can't fetch package info, skip code analysis
|
|
100
|
+
return issues;
|
|
101
|
+
}
|
|
102
|
+
// Quick check on package.json scripts first
|
|
103
|
+
if (Object.keys(packageInfo.scripts).length > 0) {
|
|
104
|
+
const scriptIssues = scanScripts(packageInfo.scripts);
|
|
105
|
+
issues.push(...scriptIssues);
|
|
106
|
+
}
|
|
107
|
+
// Check if package has lifecycle scripts that warrant deeper analysis
|
|
108
|
+
const hasLifecycleScripts = ['preinstall', 'install', 'postinstall'].some(s => s in packageInfo.scripts);
|
|
109
|
+
// If there are lifecycle scripts or already found issues, do deep scan
|
|
110
|
+
if (hasLifecycleScripts || issues.length > 0) {
|
|
111
|
+
const packageFiles = await extractAndScanPackage(packageName);
|
|
112
|
+
if (packageFiles) {
|
|
113
|
+
// Scan all collected JS files
|
|
114
|
+
for (const file of packageFiles.allJsFiles) {
|
|
115
|
+
const fileIssues = scanFileContent(file.path, file.content);
|
|
116
|
+
issues.push(...fileIssues);
|
|
117
|
+
}
|
|
118
|
+
// Scan entry file if not already scanned
|
|
119
|
+
if (packageFiles.entryContent) {
|
|
120
|
+
const entryPath = packageFiles.entryFile || 'index.js';
|
|
121
|
+
const alreadyScanned = packageFiles.allJsFiles.some(f => f.path === entryPath);
|
|
122
|
+
if (!alreadyScanned) {
|
|
123
|
+
const entryIssues = scanFileContent(entryPath, packageFiles.entryContent);
|
|
124
|
+
issues.push(...entryIssues);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return issues;
|
|
130
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { ScanResult, ScanOptions, ProgressCallback } from './types.js';
|
|
2
|
+
export declare function scanPackage(packageName: string, options?: ScanOptions, onProgress?: ProgressCallback): Promise<ScanResult>;
|
|
3
|
+
export type { ScanResult, ScanOptions, ScanIssue, RiskLevel, ProgressCallback } from './types.js';
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { scanVirusTotal } from './virustotal.js';
|
|
2
|
+
import { scanTyposquatting, findClosestPopularPackage } from './typosquatting.js';
|
|
3
|
+
import { scanCodePatterns } from './code-analyzer.js';
|
|
4
|
+
import { scanVulnerabilities } from './vulnerability.js';
|
|
5
|
+
import { checkPackageExists } from '../utils/npm-package.js';
|
|
6
|
+
import { t } from '../i18n/index.js';
|
|
7
|
+
// Built-in trusted packages
|
|
8
|
+
const TRUSTED_PACKAGES = new Set([
|
|
9
|
+
'deepv-code'
|
|
10
|
+
]);
|
|
11
|
+
export async function scanPackage(packageName, options = {}, onProgress) {
|
|
12
|
+
// Check whitelist first
|
|
13
|
+
if (TRUSTED_PACKAGES.has(packageName)) {
|
|
14
|
+
onProgress?.('Checking whitelist...', 1, 1);
|
|
15
|
+
return {
|
|
16
|
+
packageName,
|
|
17
|
+
riskLevel: 'safe',
|
|
18
|
+
issues: [],
|
|
19
|
+
checks: [
|
|
20
|
+
{ name: 'Whitelist Check', status: 'pass', description: 'Package is in the built-in trusted list' },
|
|
21
|
+
{ name: 'VirusTotal Scan', status: 'pass', description: 'Implicitly passed (Trusted)' },
|
|
22
|
+
{ name: 'Typosquatting Check', status: 'pass', description: 'Implicitly passed (Trusted)' },
|
|
23
|
+
{ name: 'Code Analysis', status: 'pass', description: 'Implicitly passed (Trusted)' },
|
|
24
|
+
{ name: 'Vulnerability Check', status: 'pass', description: 'Implicitly passed (Trusted)' }
|
|
25
|
+
],
|
|
26
|
+
canBypass: true,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// Check if package exists in registry
|
|
30
|
+
onProgress?.('Checking registry...', 0, 1);
|
|
31
|
+
const exists = await checkPackageExists(packageName);
|
|
32
|
+
if (!exists) {
|
|
33
|
+
const suggestion = findClosestPopularPackage(packageName);
|
|
34
|
+
return {
|
|
35
|
+
packageName,
|
|
36
|
+
riskLevel: 'fatal', // Treat as fatal to stop installation flow
|
|
37
|
+
issues: [{
|
|
38
|
+
type: 'typosquat',
|
|
39
|
+
severity: 'fatal',
|
|
40
|
+
message: t('packageNotFound'),
|
|
41
|
+
details: suggestion ? `${t('didYouMean')} "${suggestion}"?` : t('checkName')
|
|
42
|
+
}],
|
|
43
|
+
checks: [
|
|
44
|
+
{ name: 'Registry Check', status: 'fail', description: t('registryCheckFail') }
|
|
45
|
+
],
|
|
46
|
+
canBypass: false,
|
|
47
|
+
suggestedPackage: suggestion || undefined
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const issues = [];
|
|
51
|
+
const checks = [];
|
|
52
|
+
// Helper to wrap tasks with progress
|
|
53
|
+
let completedTasks = 0;
|
|
54
|
+
const totalTasks = 4; // VT, Typo, Code, Vuln
|
|
55
|
+
const wrapTask = async (name, task) => {
|
|
56
|
+
onProgress?.(`Starting ${name}...`, completedTasks, totalTasks);
|
|
57
|
+
try {
|
|
58
|
+
const res = await task;
|
|
59
|
+
completedTasks++;
|
|
60
|
+
onProgress?.(`Finished ${name}`, completedTasks, totalTasks);
|
|
61
|
+
return res;
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
completedTasks++;
|
|
65
|
+
onProgress?.(`Error in ${name}`, completedTasks, totalTasks);
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
// Run all scans in parallel
|
|
70
|
+
const [virusResult, typoResult, codeResult, vulnResult] = await Promise.all([
|
|
71
|
+
wrapTask('VirusTotal', options.skipVirustotal ? Promise.resolve(null) : scanVirusTotal(packageName, options).catch(err => {
|
|
72
|
+
console.error('VT Error:', err);
|
|
73
|
+
return null;
|
|
74
|
+
})),
|
|
75
|
+
wrapTask('Typosquatting Check', scanTyposquatting(packageName).catch(err => {
|
|
76
|
+
console.error('Typo Error:', err);
|
|
77
|
+
return [];
|
|
78
|
+
})),
|
|
79
|
+
wrapTask('Code Analysis', scanCodePatterns(packageName).catch(err => {
|
|
80
|
+
console.error('Code Error:', err);
|
|
81
|
+
return [];
|
|
82
|
+
})),
|
|
83
|
+
wrapTask('Vulnerability Check', scanVulnerabilities(packageName).catch(err => {
|
|
84
|
+
console.error('Vuln Error:', err);
|
|
85
|
+
return [];
|
|
86
|
+
})),
|
|
87
|
+
]);
|
|
88
|
+
// Process VirusTotal
|
|
89
|
+
if (options.skipVirustotal) {
|
|
90
|
+
checks.push({ name: 'VirusTotal Scan', status: 'skipped', description: 'Skipped by user option' });
|
|
91
|
+
}
|
|
92
|
+
else if (virusResult) {
|
|
93
|
+
// If it's the new object structure
|
|
94
|
+
const vtIssues = 'issues' in virusResult ? virusResult.issues : virusResult;
|
|
95
|
+
const vtInfo = 'info' in virusResult ? virusResult.info : 'Scan complete';
|
|
96
|
+
if (vtIssues && vtIssues.length > 0) {
|
|
97
|
+
checks.push({ name: 'VirusTotal Scan', status: 'fail', description: vtInfo });
|
|
98
|
+
issues.push(...vtIssues);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
checks.push({ name: 'VirusTotal Scan', status: 'pass', description: vtInfo });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Should not happen if catch returns null, but just in case
|
|
106
|
+
checks.push({ name: 'VirusTotal Scan', status: 'error', description: 'Scan failed to execute' });
|
|
107
|
+
}
|
|
108
|
+
// Process Typosquatting
|
|
109
|
+
if (typoResult && typoResult.length > 0) {
|
|
110
|
+
checks.push({ name: 'Typosquatting Check', status: 'fail', description: 'Suspicious package name detected' });
|
|
111
|
+
issues.push(...typoResult);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
checks.push({ name: 'Typosquatting Check', status: 'pass', description: 'Package name looks safe' });
|
|
115
|
+
}
|
|
116
|
+
// Process Code Patterns
|
|
117
|
+
if (codeResult && codeResult.length > 0) {
|
|
118
|
+
checks.push({ name: 'Static Code Analysis', status: 'fail', description: 'Suspicious code patterns detected' });
|
|
119
|
+
issues.push(...codeResult);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
checks.push({ name: 'Static Code Analysis', status: 'pass', description: 'No malicious code patterns found' });
|
|
123
|
+
}
|
|
124
|
+
// Process Vulnerabilities
|
|
125
|
+
if (vulnResult && vulnResult.length > 0) {
|
|
126
|
+
checks.push({ name: 'Vulnerability Database', status: 'fail', description: `Found ${vulnResult.length} known vulnerabilities` });
|
|
127
|
+
issues.push(...vulnResult);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
checks.push({ name: 'Vulnerability Database', status: 'pass', description: 'No known vulnerabilities (CVEs)' });
|
|
131
|
+
}
|
|
132
|
+
// Determine overall risk level
|
|
133
|
+
let riskLevel = 'safe';
|
|
134
|
+
let canBypass = true;
|
|
135
|
+
let suggestedPackage;
|
|
136
|
+
for (const issue of issues) {
|
|
137
|
+
if (issue.severity === 'fatal') {
|
|
138
|
+
riskLevel = 'fatal';
|
|
139
|
+
canBypass = false;
|
|
140
|
+
// Try to extract suggestion from typosquat details
|
|
141
|
+
if (issue.type === 'typosquat' && issue.details?.includes('"')) {
|
|
142
|
+
const match = issue.details.match(/"([^"]+)"/);
|
|
143
|
+
if (match)
|
|
144
|
+
suggestedPackage = match[1];
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
if (issue.severity === 'high') {
|
|
149
|
+
riskLevel = 'high';
|
|
150
|
+
}
|
|
151
|
+
if (issue.severity === 'warning' && riskLevel === 'safe') {
|
|
152
|
+
riskLevel = 'warning';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
packageName,
|
|
157
|
+
riskLevel,
|
|
158
|
+
issues,
|
|
159
|
+
checks,
|
|
160
|
+
canBypass,
|
|
161
|
+
suggestedPackage,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const exfiltrationPatterns = [
|
|
2
|
+
// Environment variable access + network
|
|
3
|
+
{
|
|
4
|
+
pattern: /process\.env\b/,
|
|
5
|
+
context: /(fetch|axios|http|https|request|got)\s*\(/i,
|
|
6
|
+
message: 'Environment variables accessed with network request',
|
|
7
|
+
},
|
|
8
|
+
// SSH key access
|
|
9
|
+
{
|
|
10
|
+
pattern: /\.ssh[\/\\]/,
|
|
11
|
+
context: /(readFile|readFileSync|createReadStream)/,
|
|
12
|
+
message: 'Accessing SSH directory',
|
|
13
|
+
},
|
|
14
|
+
// npmrc access
|
|
15
|
+
{
|
|
16
|
+
pattern: /\.npmrc/,
|
|
17
|
+
context: /(readFile|readFileSync|createReadStream)/,
|
|
18
|
+
message: 'Accessing .npmrc file',
|
|
19
|
+
},
|
|
20
|
+
// Base64 encoding of sensitive data
|
|
21
|
+
{
|
|
22
|
+
pattern: /Buffer\.from\(.*process\.env/,
|
|
23
|
+
context: /toString\s*\(\s*['"]base64['"]\s*\)/,
|
|
24
|
+
message: 'Base64 encoding environment data',
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
export const suspiciousNetworkPatterns = [
|
|
28
|
+
// Direct IP connections
|
|
29
|
+
/https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
30
|
+
// Suspicious TLDs
|
|
31
|
+
/https?:\/\/[^\/]+\.(tk|ml|ga|cf|gq)\//,
|
|
32
|
+
// Hex/base64 encoded URLs
|
|
33
|
+
/atob\s*\(\s*['"][A-Za-z0-9+\/=]+['"]\s*\)/,
|
|
34
|
+
/Buffer\.from\s*\(\s*['"][A-Za-z0-9+\/=]+['"]\s*,\s*['"]base64['"]\s*\)/,
|
|
35
|
+
];
|
|
36
|
+
export function checkExfiltrationPatterns(code) {
|
|
37
|
+
const findings = [];
|
|
38
|
+
for (const { pattern, context, message } of exfiltrationPatterns) {
|
|
39
|
+
if (pattern.test(code) && context.test(code)) {
|
|
40
|
+
findings.push(message);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const pattern of suspiciousNetworkPatterns) {
|
|
44
|
+
if (pattern.test(code)) {
|
|
45
|
+
findings.push(`Suspicious network pattern: ${pattern.source}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return findings;
|
|
49
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const minerPatterns = [
|
|
2
|
+
// Known miner libraries/functions
|
|
3
|
+
/cryptonight/i,
|
|
4
|
+
/coinhive/i,
|
|
5
|
+
/coin-?hive/i,
|
|
6
|
+
/minero/i,
|
|
7
|
+
/deepMiner/i,
|
|
8
|
+
/coinimp/i,
|
|
9
|
+
/crypto-?loot/i,
|
|
10
|
+
/webminer/i,
|
|
11
|
+
/mineralt/i,
|
|
12
|
+
// Mining pool connections
|
|
13
|
+
/stratum\+tcp:\/\//i,
|
|
14
|
+
/pool\..*\.(com|net|org):\d+/i,
|
|
15
|
+
/xmr\.pool/i,
|
|
16
|
+
/monero.*pool/i,
|
|
17
|
+
// WebAssembly mining patterns
|
|
18
|
+
/cryptonight.*wasm/i,
|
|
19
|
+
/cn-?lite/i,
|
|
20
|
+
// CPU/GPU mining indicators
|
|
21
|
+
/startMining/i,
|
|
22
|
+
/miner\.start/i,
|
|
23
|
+
/CryptoNoter/i,
|
|
24
|
+
];
|
|
25
|
+
export function checkMinerPatterns(code) {
|
|
26
|
+
for (const pattern of minerPatterns) {
|
|
27
|
+
if (pattern.test(code)) {
|
|
28
|
+
return { matched: true, pattern: pattern.source };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { matched: false };
|
|
32
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patterns for detecting code obfuscation
|
|
3
|
+
* Obfuscated code is often used to hide malicious behavior
|
|
4
|
+
*/
|
|
5
|
+
export declare const obfuscationPatterns: {
|
|
6
|
+
pattern: RegExp;
|
|
7
|
+
name: string;
|
|
8
|
+
}[];
|
|
9
|
+
/**
|
|
10
|
+
* Analyze code for obfuscation patterns
|
|
11
|
+
*/
|
|
12
|
+
export declare function checkObfuscationPatterns(code: string): {
|
|
13
|
+
matched: boolean;
|
|
14
|
+
pattern?: string;
|
|
15
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patterns for detecting code obfuscation
|
|
3
|
+
* Obfuscated code is often used to hide malicious behavior
|
|
4
|
+
*/
|
|
5
|
+
export const obfuscationPatterns = [
|
|
6
|
+
// Hex encoded strings (common in obfuscated code)
|
|
7
|
+
{
|
|
8
|
+
pattern: /\\x[0-9a-f]{2}(?:\\x[0-9a-f]{2}){10,}/i,
|
|
9
|
+
name: 'Hex-encoded string sequence',
|
|
10
|
+
},
|
|
11
|
+
// Unicode escape sequences (excessive use)
|
|
12
|
+
{
|
|
13
|
+
pattern: /\\u[0-9a-f]{4}(?:\\u[0-9a-f]{4}){10,}/i,
|
|
14
|
+
name: 'Unicode escape sequence',
|
|
15
|
+
},
|
|
16
|
+
// Base64 with eval
|
|
17
|
+
{
|
|
18
|
+
pattern: /eval\s*\(\s*(?:atob|Buffer\.from)\s*\(/i,
|
|
19
|
+
name: 'eval() with Base64 decode',
|
|
20
|
+
},
|
|
21
|
+
// Long strings without spaces (typical of encoded payloads)
|
|
22
|
+
{
|
|
23
|
+
pattern: /['"][A-Za-z0-9+\/=]{200,}['"]/,
|
|
24
|
+
name: 'Long Base64-like string',
|
|
25
|
+
},
|
|
26
|
+
// Function constructor with string (code execution)
|
|
27
|
+
{
|
|
28
|
+
pattern: /new\s+Function\s*\(\s*['"].*['"]\s*\)/,
|
|
29
|
+
name: 'new Function() with string code',
|
|
30
|
+
},
|
|
31
|
+
// Common obfuscator patterns
|
|
32
|
+
{
|
|
33
|
+
pattern: /_0x[a-f0-9]{4,}/i,
|
|
34
|
+
name: 'JavaScript obfuscator variable pattern',
|
|
35
|
+
},
|
|
36
|
+
// Array-based string obfuscation
|
|
37
|
+
{
|
|
38
|
+
pattern: /\[['"][^'"]{1,20}['"](?:\s*,\s*['"][^'"]{1,20}['"]\s*){20,}\]/,
|
|
39
|
+
name: 'Array-based string obfuscation',
|
|
40
|
+
},
|
|
41
|
+
// Bitwise operations for string decoding
|
|
42
|
+
{
|
|
43
|
+
pattern: /String\.fromCharCode\s*\(\s*(?:\d+\s*[\^&|]\s*)+\d+\s*\)/,
|
|
44
|
+
name: 'Bitwise string decoding',
|
|
45
|
+
},
|
|
46
|
+
// JSFuck patterns
|
|
47
|
+
{
|
|
48
|
+
pattern: /\[\]\[(!\[\]\+\[\])/,
|
|
49
|
+
name: 'JSFuck obfuscation',
|
|
50
|
+
},
|
|
51
|
+
// eval with string concatenation/manipulation
|
|
52
|
+
{
|
|
53
|
+
pattern: /eval\s*\(\s*(?:\w+\s*\+\s*)+\w+\s*\)/,
|
|
54
|
+
name: 'eval() with string concatenation',
|
|
55
|
+
},
|
|
56
|
+
// setTimeout/setInterval with string
|
|
57
|
+
{
|
|
58
|
+
pattern: /set(?:Timeout|Interval)\s*\(\s*['"][^'"]+['"]/,
|
|
59
|
+
name: 'setTimeout/setInterval with string code',
|
|
60
|
+
},
|
|
61
|
+
// Char code array to string
|
|
62
|
+
{
|
|
63
|
+
pattern: /String\.fromCharCode\.apply\s*\(\s*null\s*,/,
|
|
64
|
+
name: 'Char code array conversion',
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
// Threshold for considering code as obfuscated
|
|
68
|
+
const OBFUSCATION_THRESHOLD = 0.3; // 30% of lines look obfuscated
|
|
69
|
+
/**
|
|
70
|
+
* Analyze code for obfuscation patterns
|
|
71
|
+
*/
|
|
72
|
+
export function checkObfuscationPatterns(code) {
|
|
73
|
+
// Check for specific obfuscation patterns
|
|
74
|
+
for (const { pattern, name } of obfuscationPatterns) {
|
|
75
|
+
if (pattern.test(code)) {
|
|
76
|
+
return { matched: true, pattern: name };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Heuristic: check for high entropy (many short variable names)
|
|
80
|
+
const lines = code.split('\n').filter(line => line.trim().length > 0);
|
|
81
|
+
if (lines.length < 10) {
|
|
82
|
+
return { matched: false };
|
|
83
|
+
}
|
|
84
|
+
// Count lines with suspicious patterns
|
|
85
|
+
let suspiciousLines = 0;
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
// Very long line without spaces
|
|
88
|
+
if (line.length > 500 && line.split(/\s+/).length < 10) {
|
|
89
|
+
suspiciousLines++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
// Many short variable names (a, b, c, _0x...)
|
|
93
|
+
const shortVarMatches = line.match(/\b[a-z_$][a-z0-9_$]?\b/gi);
|
|
94
|
+
if (shortVarMatches && shortVarMatches.length > 20) {
|
|
95
|
+
suspiciousLines++;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// Excessive bracket/parenthesis nesting
|
|
99
|
+
const brackets = (line.match(/[\[\]\(\)\{\}]/g) || []).length;
|
|
100
|
+
if (brackets > 50) {
|
|
101
|
+
suspiciousLines++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const obfuscationRatio = suspiciousLines / lines.length;
|
|
106
|
+
if (obfuscationRatio > OBFUSCATION_THRESHOLD) {
|
|
107
|
+
return { matched: true, pattern: `Heuristic: ${Math.round(obfuscationRatio * 100)}% of code appears obfuscated` };
|
|
108
|
+
}
|
|
109
|
+
return { matched: false };
|
|
110
|
+
}
|