devcompass 2.6.0 → 2.7.1
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 +389 -96
- package/data/known-malicious.json +57 -0
- package/package.json +12 -4
- package/src/analyzers/license-risk.js +225 -0
- package/src/analyzers/package-quality.js +368 -0
- package/src/analyzers/security-recommendations.js +274 -0
- package/src/analyzers/supply-chain.js +223 -0
- package/src/commands/analyze.js +447 -18
- package/src/utils/json-formatter.js +118 -28
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// src/analyzers/security-recommendations.js
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Priority levels for recommendations
|
|
5
|
+
*/
|
|
6
|
+
const PRIORITY = {
|
|
7
|
+
CRITICAL: { level: 1, label: 'CRITICAL', color: 'red', emoji: '🔴' },
|
|
8
|
+
HIGH: { level: 2, label: 'HIGH', color: 'orange', emoji: '🟠' },
|
|
9
|
+
MEDIUM: { level: 3, label: 'MEDIUM', color: 'yellow', emoji: '🟡' },
|
|
10
|
+
LOW: { level: 4, label: 'LOW', color: 'gray', emoji: '⚪' }
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate actionable recommendations from all security findings
|
|
15
|
+
*/
|
|
16
|
+
function generateSecurityRecommendations(analysisResults) {
|
|
17
|
+
const recommendations = [];
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
supplyChainWarnings = [],
|
|
21
|
+
licenseWarnings = [],
|
|
22
|
+
qualityResults = [],
|
|
23
|
+
securityVulnerabilities = [],
|
|
24
|
+
ecosystemAlerts = [],
|
|
25
|
+
unusedDeps = [],
|
|
26
|
+
outdatedPackages = []
|
|
27
|
+
} = analysisResults;
|
|
28
|
+
|
|
29
|
+
// 1. Supply Chain Issues (CRITICAL/HIGH)
|
|
30
|
+
for (const warning of supplyChainWarnings) {
|
|
31
|
+
if (warning.type === 'malicious' || warning.severity === 'critical') {
|
|
32
|
+
recommendations.push({
|
|
33
|
+
priority: PRIORITY.CRITICAL,
|
|
34
|
+
category: 'supply_chain',
|
|
35
|
+
package: warning.package,
|
|
36
|
+
issue: warning.message,
|
|
37
|
+
action: `Remove ${warning.package} immediately`,
|
|
38
|
+
command: `npm uninstall ${warning.package}`,
|
|
39
|
+
impact: 'Eliminates critical security risk',
|
|
40
|
+
reason: 'Known malicious package detected'
|
|
41
|
+
});
|
|
42
|
+
} else if (warning.type === 'typosquatting') {
|
|
43
|
+
recommendations.push({
|
|
44
|
+
priority: PRIORITY.HIGH,
|
|
45
|
+
category: 'supply_chain',
|
|
46
|
+
package: warning.package,
|
|
47
|
+
issue: warning.message,
|
|
48
|
+
action: warning.recommendation,
|
|
49
|
+
command: `npm uninstall ${warning.package} && npm install ${warning.official}`,
|
|
50
|
+
impact: 'Prevents potential supply chain attack',
|
|
51
|
+
reason: 'Typosquatting attempt detected'
|
|
52
|
+
});
|
|
53
|
+
} else if (warning.type === 'install_script' && warning.severity === 'high') {
|
|
54
|
+
recommendations.push({
|
|
55
|
+
priority: PRIORITY.HIGH,
|
|
56
|
+
category: 'supply_chain',
|
|
57
|
+
package: warning.package,
|
|
58
|
+
issue: warning.message,
|
|
59
|
+
action: 'Review install script before deployment',
|
|
60
|
+
command: `cat node_modules/${warning.package.split('@')[0]}/package.json`,
|
|
61
|
+
impact: 'Prevents malicious code execution',
|
|
62
|
+
reason: 'Suspicious install script detected'
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. License Compliance (HIGH/MEDIUM)
|
|
68
|
+
for (const warning of licenseWarnings) {
|
|
69
|
+
if (warning.severity === 'critical' || warning.severity === 'high') {
|
|
70
|
+
recommendations.push({
|
|
71
|
+
priority: warning.severity === 'critical' ? PRIORITY.CRITICAL : PRIORITY.HIGH,
|
|
72
|
+
category: 'license',
|
|
73
|
+
package: warning.package,
|
|
74
|
+
issue: `${warning.license}: ${warning.message}`,
|
|
75
|
+
action: 'Replace with permissive alternative',
|
|
76
|
+
command: `npm uninstall ${warning.package}`,
|
|
77
|
+
impact: 'Ensures license compliance',
|
|
78
|
+
reason: 'High-risk license detected',
|
|
79
|
+
alternative: 'Search npm for MIT/Apache alternatives'
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 3. Security Vulnerabilities (CRITICAL/HIGH)
|
|
85
|
+
if (securityVulnerabilities.critical > 0 || securityVulnerabilities.high > 0) {
|
|
86
|
+
recommendations.push({
|
|
87
|
+
priority: securityVulnerabilities.critical > 0 ? PRIORITY.CRITICAL : PRIORITY.HIGH,
|
|
88
|
+
category: 'security',
|
|
89
|
+
issue: `${securityVulnerabilities.total} security vulnerabilities detected`,
|
|
90
|
+
action: 'Run npm audit fix to resolve vulnerabilities',
|
|
91
|
+
command: 'npm audit fix',
|
|
92
|
+
impact: `Resolves ${securityVulnerabilities.total} known vulnerabilities`,
|
|
93
|
+
reason: 'Security vulnerabilities in dependencies'
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 4. Ecosystem Alerts (varies by severity)
|
|
98
|
+
for (const alert of ecosystemAlerts) {
|
|
99
|
+
if (alert.severity === 'critical' || alert.severity === 'high') {
|
|
100
|
+
const priority = alert.severity === 'critical' ? PRIORITY.CRITICAL : PRIORITY.HIGH;
|
|
101
|
+
|
|
102
|
+
recommendations.push({
|
|
103
|
+
priority: priority,
|
|
104
|
+
category: 'ecosystem',
|
|
105
|
+
package: `${alert.package}@${alert.version}`,
|
|
106
|
+
issue: alert.title,
|
|
107
|
+
action: `Upgrade to ${alert.fix}`,
|
|
108
|
+
command: `npm install ${alert.package}@${alert.fix}`,
|
|
109
|
+
impact: 'Resolves known stability/security issue',
|
|
110
|
+
reason: alert.source
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 5. Package Quality Issues (MEDIUM/LOW)
|
|
116
|
+
for (const pkg of qualityResults) {
|
|
117
|
+
if (pkg.status === 'deprecated') {
|
|
118
|
+
recommendations.push({
|
|
119
|
+
priority: PRIORITY.CRITICAL,
|
|
120
|
+
category: 'quality',
|
|
121
|
+
package: pkg.package,
|
|
122
|
+
issue: 'Package is deprecated',
|
|
123
|
+
action: 'Find actively maintained alternative',
|
|
124
|
+
command: `npm uninstall ${pkg.package}`,
|
|
125
|
+
impact: 'Prevents future breaking changes',
|
|
126
|
+
reason: 'Package is no longer maintained',
|
|
127
|
+
healthScore: pkg.healthScore
|
|
128
|
+
});
|
|
129
|
+
} else if (pkg.status === 'abandoned') {
|
|
130
|
+
recommendations.push({
|
|
131
|
+
priority: PRIORITY.HIGH,
|
|
132
|
+
category: 'quality',
|
|
133
|
+
package: pkg.package,
|
|
134
|
+
issue: `Last updated ${Math.floor(pkg.daysSincePublish / 365)} years ago`,
|
|
135
|
+
action: 'Migrate to actively maintained alternative',
|
|
136
|
+
command: `npm uninstall ${pkg.package}`,
|
|
137
|
+
impact: 'Improves long-term stability',
|
|
138
|
+
reason: 'Package appears abandoned',
|
|
139
|
+
healthScore: pkg.healthScore
|
|
140
|
+
});
|
|
141
|
+
} else if (pkg.status === 'stale' && pkg.healthScore < 5) {
|
|
142
|
+
recommendations.push({
|
|
143
|
+
priority: PRIORITY.MEDIUM,
|
|
144
|
+
category: 'quality',
|
|
145
|
+
package: pkg.package,
|
|
146
|
+
issue: `Health score: ${pkg.healthScore}/10`,
|
|
147
|
+
action: 'Consider more actively maintained alternative',
|
|
148
|
+
impact: 'Improves package quality',
|
|
149
|
+
reason: 'Low health score',
|
|
150
|
+
healthScore: pkg.healthScore
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 6. Unused Dependencies (MEDIUM)
|
|
156
|
+
if (unusedDeps.length > 0) {
|
|
157
|
+
const packageList = unusedDeps.map(d => d.name).join(' ');
|
|
158
|
+
recommendations.push({
|
|
159
|
+
priority: PRIORITY.MEDIUM,
|
|
160
|
+
category: 'cleanup',
|
|
161
|
+
issue: `${unusedDeps.length} unused dependencies detected`,
|
|
162
|
+
action: 'Remove unused packages',
|
|
163
|
+
command: `npm uninstall ${packageList}`,
|
|
164
|
+
impact: `Reduces node_modules size, improves security surface`,
|
|
165
|
+
reason: 'Unused dependencies increase attack surface'
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 7. Outdated Packages (LOW)
|
|
170
|
+
const criticalOutdated = outdatedPackages.filter(p =>
|
|
171
|
+
p.updateType === 'major update' && p.current.startsWith('0.')
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (criticalOutdated.length > 0) {
|
|
175
|
+
for (const pkg of criticalOutdated.slice(0, 3)) { // Top 3
|
|
176
|
+
recommendations.push({
|
|
177
|
+
priority: PRIORITY.MEDIUM,
|
|
178
|
+
category: 'updates',
|
|
179
|
+
package: pkg.name,
|
|
180
|
+
issue: `Version ${pkg.current} is pre-1.0 and outdated`,
|
|
181
|
+
action: `Update to ${pkg.latest}`,
|
|
182
|
+
command: `npm install ${pkg.name}@latest`,
|
|
183
|
+
impact: 'Gets bug fixes and improvements',
|
|
184
|
+
reason: 'Pre-1.0 packages change rapidly'
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Sort by priority
|
|
190
|
+
recommendations.sort((a, b) => a.priority.level - b.priority.level);
|
|
191
|
+
|
|
192
|
+
return recommendations;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Group recommendations by priority
|
|
197
|
+
*/
|
|
198
|
+
function groupByPriority(recommendations) {
|
|
199
|
+
return {
|
|
200
|
+
critical: recommendations.filter(r => r.priority.level === 1),
|
|
201
|
+
high: recommendations.filter(r => r.priority.level === 2),
|
|
202
|
+
medium: recommendations.filter(r => r.priority.level === 3),
|
|
203
|
+
low: recommendations.filter(r => r.priority.level === 4)
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Calculate expected impact of following recommendations
|
|
209
|
+
*/
|
|
210
|
+
function calculateExpectedImpact(recommendations, currentScore) {
|
|
211
|
+
let improvement = 0;
|
|
212
|
+
|
|
213
|
+
for (const rec of recommendations) {
|
|
214
|
+
switch (rec.priority.level) {
|
|
215
|
+
case 1: // CRITICAL
|
|
216
|
+
improvement += 2.0;
|
|
217
|
+
break;
|
|
218
|
+
case 2: // HIGH
|
|
219
|
+
improvement += 1.5;
|
|
220
|
+
break;
|
|
221
|
+
case 3: // MEDIUM
|
|
222
|
+
improvement += 0.5;
|
|
223
|
+
break;
|
|
224
|
+
case 4: // LOW
|
|
225
|
+
improvement += 0.2;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const newScore = Math.min(10, currentScore + improvement);
|
|
231
|
+
const percentageIncrease = ((newScore - currentScore) / 10) * 100;
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
currentScore,
|
|
235
|
+
expectedScore: Number(newScore.toFixed(1)),
|
|
236
|
+
improvement: Number(improvement.toFixed(1)),
|
|
237
|
+
percentageIncrease: Number(percentageIncrease.toFixed(1)),
|
|
238
|
+
critical: recommendations.filter(r => r.priority.level === 1).length,
|
|
239
|
+
high: recommendations.filter(r => r.priority.level === 2).length,
|
|
240
|
+
medium: recommendations.filter(r => r.priority.level === 3).length,
|
|
241
|
+
low: recommendations.filter(r => r.priority.level === 4).length
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get top N recommendations
|
|
247
|
+
*/
|
|
248
|
+
function getTopRecommendations(recommendations, n = 5) {
|
|
249
|
+
return recommendations.slice(0, n);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get recommendations by category
|
|
254
|
+
*/
|
|
255
|
+
function getRecommendationsByCategory(recommendations) {
|
|
256
|
+
return {
|
|
257
|
+
supply_chain: recommendations.filter(r => r.category === 'supply_chain'),
|
|
258
|
+
license: recommendations.filter(r => r.category === 'license'),
|
|
259
|
+
security: recommendations.filter(r => r.category === 'security'),
|
|
260
|
+
ecosystem: recommendations.filter(r => r.category === 'ecosystem'),
|
|
261
|
+
quality: recommendations.filter(r => r.category === 'quality'),
|
|
262
|
+
cleanup: recommendations.filter(r => r.category === 'cleanup'),
|
|
263
|
+
updates: recommendations.filter(r => r.category === 'updates')
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = {
|
|
268
|
+
generateSecurityRecommendations,
|
|
269
|
+
groupByPriority,
|
|
270
|
+
calculateExpectedImpact,
|
|
271
|
+
getTopRecommendations,
|
|
272
|
+
getRecommendationsByCategory,
|
|
273
|
+
PRIORITY
|
|
274
|
+
};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// src/analyzers/supply-chain.js
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load known malicious packages database
|
|
7
|
+
*/
|
|
8
|
+
function loadMaliciousDatabase() {
|
|
9
|
+
try {
|
|
10
|
+
const dbPath = path.join(__dirname, '../../data/known-malicious.json');
|
|
11
|
+
const data = fs.readFileSync(dbPath, 'utf8');
|
|
12
|
+
return JSON.parse(data);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error('Error loading malicious packages database:', error.message);
|
|
15
|
+
return {
|
|
16
|
+
malicious_packages: [],
|
|
17
|
+
typosquat_patterns: {},
|
|
18
|
+
suspicious_patterns: { install_scripts: [], suspicious_dependencies: [] }
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Calculate Levenshtein distance between two strings
|
|
25
|
+
*/
|
|
26
|
+
function levenshteinDistance(str1, str2) {
|
|
27
|
+
const matrix = [];
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i <= str2.length; i++) {
|
|
30
|
+
matrix[i] = [i];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (let j = 0; j <= str1.length; j++) {
|
|
34
|
+
matrix[0][j] = j;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (let i = 1; i <= str2.length; i++) {
|
|
38
|
+
for (let j = 1; j <= str1.length; j++) {
|
|
39
|
+
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
|
|
40
|
+
matrix[i][j] = matrix[i - 1][j - 1];
|
|
41
|
+
} else {
|
|
42
|
+
matrix[i][j] = Math.min(
|
|
43
|
+
matrix[i - 1][j - 1] + 1,
|
|
44
|
+
matrix[i][j - 1] + 1,
|
|
45
|
+
matrix[i - 1][j] + 1
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return matrix[str2.length][str1.length];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Detect typosquatting attempts
|
|
56
|
+
*/
|
|
57
|
+
function detectTyposquatting(packageName, packageVersion, maliciousDb) {
|
|
58
|
+
const warnings = [];
|
|
59
|
+
|
|
60
|
+
// Whitelist of legitimate popular packages that might have similar names
|
|
61
|
+
// These should NEVER be flagged as typosquatting
|
|
62
|
+
const LEGITIMATE_PACKAGES = [
|
|
63
|
+
'chalk', 'chai', 'ora', 'yargs', 'meow', 'execa', 'globby', 'del', 'make-dir',
|
|
64
|
+
'p-map', 'p-limit', 'p-queue', 'got', 'ky', 'node-fetch', 'cross-fetch',
|
|
65
|
+
'uuid', 'nanoid', 'cuid', 'luxon', 'date-fns', 'ms', 'bytes', 'filesize',
|
|
66
|
+
'fast-glob', 'chokidar', 'picomatch', 'micromatch', 'anymatch',
|
|
67
|
+
'semver', 'commander', 'yargs', 'inquirer', 'prompts', 'enquirer',
|
|
68
|
+
'debug', 'pino', 'winston', 'bunyan', 'signale'
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// Skip whitelist packages
|
|
72
|
+
if (LEGITIMATE_PACKAGES.includes(packageName)) {
|
|
73
|
+
return warnings;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check against known typosquat patterns
|
|
77
|
+
// typosquat_patterns is an object: { "express": ["epress", "expres"], ... }
|
|
78
|
+
const patterns = maliciousDb.typosquat_patterns || {};
|
|
79
|
+
|
|
80
|
+
for (const officialName of Object.keys(patterns)) {
|
|
81
|
+
// Skip if the official package is also whitelisted (both are legitimate)
|
|
82
|
+
// This prevents false positives like "chalk" vs "chai"
|
|
83
|
+
if (LEGITIMATE_PACKAGES.includes(officialName)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const distance = levenshteinDistance(packageName, officialName);
|
|
88
|
+
|
|
89
|
+
// Flag if 1-2 character difference (typosquatting likely)
|
|
90
|
+
if (distance > 0 && distance <= 2 && packageName !== officialName) {
|
|
91
|
+
warnings.push({
|
|
92
|
+
package: `${packageName}@${packageVersion}`,
|
|
93
|
+
type: 'typosquatting',
|
|
94
|
+
severity: distance === 1 ? 'high' : 'medium',
|
|
95
|
+
message: `Similar to: ${officialName} (official package)`,
|
|
96
|
+
recommendation: `Verify if you meant to install ${officialName}`,
|
|
97
|
+
official: officialName
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return warnings;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if package is known malicious
|
|
107
|
+
*/
|
|
108
|
+
function checkMaliciousPackage(packageName, database) {
|
|
109
|
+
if (database.malicious_packages.includes(packageName.toLowerCase())) {
|
|
110
|
+
return {
|
|
111
|
+
package: packageName,
|
|
112
|
+
type: 'malicious',
|
|
113
|
+
severity: 'critical',
|
|
114
|
+
message: `Known malicious package detected: ${packageName}`,
|
|
115
|
+
recommendation: 'Remove immediately - this package is known to be malicious'
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Analyze install scripts for suspicious patterns
|
|
123
|
+
*/
|
|
124
|
+
function analyzeInstallScripts(projectPath, dependencies) {
|
|
125
|
+
const warnings = [];
|
|
126
|
+
const database = loadMaliciousDatabase();
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
for (const dep of Object.keys(dependencies)) {
|
|
130
|
+
const packageJsonPath = path.join(projectPath, 'node_modules', dep, 'package.json');
|
|
131
|
+
|
|
132
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
137
|
+
const scripts = packageJson.scripts || {};
|
|
138
|
+
|
|
139
|
+
// Check for install hooks
|
|
140
|
+
const installHooks = ['preinstall', 'install', 'postinstall'];
|
|
141
|
+
|
|
142
|
+
for (const hook of installHooks) {
|
|
143
|
+
if (scripts[hook]) {
|
|
144
|
+
const script = scripts[hook].toLowerCase();
|
|
145
|
+
|
|
146
|
+
// Check for suspicious patterns
|
|
147
|
+
const suspiciousPatterns = database.suspicious_patterns.install_scripts;
|
|
148
|
+
const foundPatterns = suspiciousPatterns.filter(pattern =>
|
|
149
|
+
script.includes(pattern.toLowerCase())
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (foundPatterns.length > 0) {
|
|
153
|
+
warnings.push({
|
|
154
|
+
package: `${dep}@${packageJson.version}`,
|
|
155
|
+
type: 'install_script',
|
|
156
|
+
severity: foundPatterns.some(p => ['curl', 'wget', 'eval', 'exec'].includes(p)) ? 'high' : 'medium',
|
|
157
|
+
script: hook,
|
|
158
|
+
action: scripts[hook],
|
|
159
|
+
patterns: foundPatterns,
|
|
160
|
+
message: `Install script contains suspicious patterns: ${foundPatterns.join(', ')}`,
|
|
161
|
+
recommendation: 'Review the install script before deployment'
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('Error analyzing install scripts:', error.message);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return warnings;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Perform comprehensive supply chain security analysis
|
|
176
|
+
*/
|
|
177
|
+
async function analyzeSupplyChain(projectPath, dependencies) {
|
|
178
|
+
const warnings = [];
|
|
179
|
+
const database = loadMaliciousDatabase();
|
|
180
|
+
|
|
181
|
+
// Check each dependency
|
|
182
|
+
for (const [packageName, version] of Object.entries(dependencies)) {
|
|
183
|
+
// 1. Check if known malicious
|
|
184
|
+
const maliciousCheck = checkMaliciousPackage(packageName, database);
|
|
185
|
+
if (maliciousCheck) {
|
|
186
|
+
warnings.push(maliciousCheck);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 2. Detect typosquatting
|
|
190
|
+
const typosquatWarnings = detectTyposquatting(packageName, version, database);
|
|
191
|
+
warnings.push(...typosquatWarnings);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 3. Analyze install scripts
|
|
195
|
+
const scriptWarnings = analyzeInstallScripts(projectPath, dependencies);
|
|
196
|
+
warnings.push(...scriptWarnings);
|
|
197
|
+
|
|
198
|
+
return warnings;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get supply chain statistics
|
|
203
|
+
*/
|
|
204
|
+
function getSupplyChainStats(warnings) {
|
|
205
|
+
return {
|
|
206
|
+
total: warnings.length,
|
|
207
|
+
critical: warnings.filter(w => w.severity === 'critical').length,
|
|
208
|
+
high: warnings.filter(w => w.severity === 'high').length,
|
|
209
|
+
medium: warnings.filter(w => w.severity === 'medium').length,
|
|
210
|
+
malicious: warnings.filter(w => w.type === 'malicious').length,
|
|
211
|
+
typosquatting: warnings.filter(w => w.type === 'typosquatting' || w.type === 'typosquatting_suspected').length,
|
|
212
|
+
installScripts: warnings.filter(w => w.type === 'install_script').length
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
analyzeSupplyChain,
|
|
218
|
+
detectTyposquatting,
|
|
219
|
+
checkMaliciousPackage,
|
|
220
|
+
analyzeInstallScripts,
|
|
221
|
+
getSupplyChainStats,
|
|
222
|
+
loadMaliciousDatabase
|
|
223
|
+
};
|