devcompass 2.5.0 → 2.7.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 +400 -93
- package/data/known-malicious.json +57 -0
- package/package.json +13 -3
- package/src/alerts/github-tracker.js +53 -19
- package/src/alerts/predictive.js +10 -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 +217 -0
- package/src/commands/analyze.js +466 -17
- package/src/utils/json-formatter.js +118 -28
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// src/analyzers/package-quality.js
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Fetch package metadata from npm registry
|
|
8
|
+
*/
|
|
9
|
+
function fetchNpmPackageInfo(packageName) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const options = {
|
|
12
|
+
hostname: 'registry.npmjs.org',
|
|
13
|
+
path: `/${packageName}`,
|
|
14
|
+
method: 'GET',
|
|
15
|
+
headers: {
|
|
16
|
+
'User-Agent': 'DevCompass',
|
|
17
|
+
'Accept': 'application/json'
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const req = https.request(options, (res) => {
|
|
22
|
+
let data = '';
|
|
23
|
+
|
|
24
|
+
res.on('data', (chunk) => {
|
|
25
|
+
data += chunk;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
res.on('end', () => {
|
|
29
|
+
if (res.statusCode === 200) {
|
|
30
|
+
try {
|
|
31
|
+
resolve(JSON.parse(data));
|
|
32
|
+
} catch (error) {
|
|
33
|
+
reject(new Error('Failed to parse npm response'));
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
reject(new Error(`npm registry returned ${res.statusCode}`));
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
req.on('error', reject);
|
|
42
|
+
req.setTimeout(5000, () => {
|
|
43
|
+
req.destroy();
|
|
44
|
+
reject(new Error('Request timeout'));
|
|
45
|
+
});
|
|
46
|
+
req.end();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Calculate days since last publish
|
|
52
|
+
*/
|
|
53
|
+
function daysSincePublish(dateString) {
|
|
54
|
+
const publishDate = new Date(dateString);
|
|
55
|
+
const now = new Date();
|
|
56
|
+
const diffTime = Math.abs(now - publishDate);
|
|
57
|
+
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
58
|
+
return diffDays;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Calculate package health score (0-10)
|
|
63
|
+
*/
|
|
64
|
+
function calculateHealthScore(packageData, githubData = null) {
|
|
65
|
+
let score = 10;
|
|
66
|
+
|
|
67
|
+
// Get latest version info
|
|
68
|
+
const latestVersion = packageData['dist-tags']?.latest;
|
|
69
|
+
const versionData = packageData.versions?.[latestVersion];
|
|
70
|
+
const time = packageData.time?.[latestVersion];
|
|
71
|
+
|
|
72
|
+
if (!versionData || !time) {
|
|
73
|
+
return 5; // Default score if data missing
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 1. Age factor (max -2 points)
|
|
77
|
+
const daysSince = daysSincePublish(time);
|
|
78
|
+
if (daysSince > 365 * 3) {
|
|
79
|
+
score -= 2; // 3+ years old
|
|
80
|
+
} else if (daysSince > 365 * 2) {
|
|
81
|
+
score -= 1.5; // 2-3 years old
|
|
82
|
+
} else if (daysSince > 365) {
|
|
83
|
+
score -= 1; // 1-2 years old
|
|
84
|
+
} else if (daysSince > 180) {
|
|
85
|
+
score -= 0.5; // 6-12 months old
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 2. Maintenance frequency (max -2 points)
|
|
89
|
+
const versions = Object.keys(packageData.versions || {});
|
|
90
|
+
const recentVersions = versions.filter(v => {
|
|
91
|
+
const vTime = packageData.time?.[v];
|
|
92
|
+
if (!vTime) return false;
|
|
93
|
+
return daysSincePublish(vTime) <= 365;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (recentVersions.length === 0) {
|
|
97
|
+
score -= 2; // No updates in a year
|
|
98
|
+
} else if (recentVersions.length < 3) {
|
|
99
|
+
score -= 1; // Less than 3 updates in a year
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 3. GitHub activity (if available) (max -2 points)
|
|
103
|
+
if (githubData) {
|
|
104
|
+
const { totalIssues, last7Days, last30Days } = githubData;
|
|
105
|
+
|
|
106
|
+
// High issue count with low activity is bad
|
|
107
|
+
if (totalIssues > 100 && last30Days < 5) {
|
|
108
|
+
score -= 1.5; // Many issues, low maintenance
|
|
109
|
+
} else if (totalIssues > 50 && last30Days < 3) {
|
|
110
|
+
score -= 1; // Medium issues, low maintenance
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Very high recent activity might indicate problems
|
|
114
|
+
if (last7Days > 30) {
|
|
115
|
+
score -= 0.5; // Unusually high activity
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 4. Dependencies count (max -1 point)
|
|
120
|
+
const deps = versionData.dependencies || {};
|
|
121
|
+
const depCount = Object.keys(deps).length;
|
|
122
|
+
|
|
123
|
+
if (depCount > 50) {
|
|
124
|
+
score -= 1; // Too many dependencies
|
|
125
|
+
} else if (depCount > 30) {
|
|
126
|
+
score -= 0.5;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 5. Has description and repository (max -1 point)
|
|
130
|
+
if (!packageData.description) {
|
|
131
|
+
score -= 0.5;
|
|
132
|
+
}
|
|
133
|
+
if (!packageData.repository) {
|
|
134
|
+
score -= 0.5;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 6. Deprecated packages (automatic 0)
|
|
138
|
+
if (versionData.deprecated) {
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return Math.max(0, Math.min(10, score));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Determine package status based on health score
|
|
147
|
+
*/
|
|
148
|
+
function getPackageStatus(score, daysSince) {
|
|
149
|
+
if (score === 0) {
|
|
150
|
+
return {
|
|
151
|
+
status: 'deprecated',
|
|
152
|
+
color: 'red',
|
|
153
|
+
severity: 'critical',
|
|
154
|
+
label: 'DEPRECATED'
|
|
155
|
+
};
|
|
156
|
+
} else if (score < 3 || daysSince > 365 * 3) {
|
|
157
|
+
return {
|
|
158
|
+
status: 'abandoned',
|
|
159
|
+
color: 'red',
|
|
160
|
+
severity: 'critical',
|
|
161
|
+
label: 'ABANDONED'
|
|
162
|
+
};
|
|
163
|
+
} else if (score < 5 || daysSince > 365 * 2) {
|
|
164
|
+
return {
|
|
165
|
+
status: 'stale',
|
|
166
|
+
color: 'yellow',
|
|
167
|
+
severity: 'high',
|
|
168
|
+
label: 'STALE'
|
|
169
|
+
};
|
|
170
|
+
} else if (score < 7) {
|
|
171
|
+
return {
|
|
172
|
+
status: 'needs_attention',
|
|
173
|
+
color: 'yellow',
|
|
174
|
+
severity: 'medium',
|
|
175
|
+
label: 'NEEDS ATTENTION'
|
|
176
|
+
};
|
|
177
|
+
} else {
|
|
178
|
+
return {
|
|
179
|
+
status: 'healthy',
|
|
180
|
+
color: 'green',
|
|
181
|
+
severity: 'low',
|
|
182
|
+
label: 'HEALTHY'
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get maintainer activity status
|
|
189
|
+
*/
|
|
190
|
+
function getMaintainerStatus(packageData) {
|
|
191
|
+
const maintainers = packageData.maintainers || [];
|
|
192
|
+
const latestVersion = packageData['dist-tags']?.latest;
|
|
193
|
+
const time = packageData.time?.[latestVersion];
|
|
194
|
+
|
|
195
|
+
if (!time) {
|
|
196
|
+
return 'unknown';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const daysSince = daysSincePublish(time);
|
|
200
|
+
|
|
201
|
+
if (daysSince > 365 * 2) {
|
|
202
|
+
return 'inactive';
|
|
203
|
+
} else if (daysSince > 365) {
|
|
204
|
+
return 'low_activity';
|
|
205
|
+
} else if (daysSince > 180) {
|
|
206
|
+
return 'moderate_activity';
|
|
207
|
+
} else {
|
|
208
|
+
return 'active';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Analyze package quality for all dependencies
|
|
214
|
+
*/
|
|
215
|
+
async function analyzePackageQuality(dependencies, githubData = []) {
|
|
216
|
+
const results = [];
|
|
217
|
+
const stats = {
|
|
218
|
+
total: 0,
|
|
219
|
+
healthy: 0,
|
|
220
|
+
needsAttention: 0,
|
|
221
|
+
stale: 0,
|
|
222
|
+
abandoned: 0,
|
|
223
|
+
deprecated: 0
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Create GitHub data lookup
|
|
227
|
+
const githubLookup = {};
|
|
228
|
+
for (const data of githubData) {
|
|
229
|
+
githubLookup[data.package] = data;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Analyze each package (limit to prevent rate limiting)
|
|
233
|
+
const packages = Object.keys(dependencies).slice(0, 20); // Analyze first 20
|
|
234
|
+
|
|
235
|
+
for (const packageName of packages) {
|
|
236
|
+
try {
|
|
237
|
+
stats.total++;
|
|
238
|
+
|
|
239
|
+
// Fetch package info from npm
|
|
240
|
+
const packageData = await fetchNpmPackageInfo(packageName);
|
|
241
|
+
|
|
242
|
+
// Calculate health score
|
|
243
|
+
const github = githubLookup[packageName];
|
|
244
|
+
const healthScore = calculateHealthScore(packageData, github);
|
|
245
|
+
|
|
246
|
+
// Get latest version info
|
|
247
|
+
const latestVersion = packageData['dist-tags']?.latest;
|
|
248
|
+
const time = packageData.time?.[latestVersion];
|
|
249
|
+
const daysSince = time ? daysSincePublish(time) : 0;
|
|
250
|
+
|
|
251
|
+
// Determine status
|
|
252
|
+
const status = getPackageStatus(healthScore, daysSince);
|
|
253
|
+
const maintainerStatus = getMaintainerStatus(packageData);
|
|
254
|
+
|
|
255
|
+
// Count by status
|
|
256
|
+
if (status.status === 'healthy') {
|
|
257
|
+
stats.healthy++;
|
|
258
|
+
} else if (status.status === 'needs_attention') {
|
|
259
|
+
stats.needsAttention++;
|
|
260
|
+
} else if (status.status === 'stale') {
|
|
261
|
+
stats.stale++;
|
|
262
|
+
} else if (status.status === 'abandoned') {
|
|
263
|
+
stats.abandoned++;
|
|
264
|
+
} else if (status.status === 'deprecated') {
|
|
265
|
+
stats.deprecated++;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Get repository info
|
|
269
|
+
const repository = packageData.repository?.url || '';
|
|
270
|
+
const hasGithub = repository.includes('github.com');
|
|
271
|
+
|
|
272
|
+
// Build result
|
|
273
|
+
const result = {
|
|
274
|
+
package: packageName,
|
|
275
|
+
version: dependencies[packageName],
|
|
276
|
+
healthScore: Number(healthScore.toFixed(1)),
|
|
277
|
+
status: status.status,
|
|
278
|
+
severity: status.severity,
|
|
279
|
+
label: status.label,
|
|
280
|
+
lastPublish: time ? new Date(time).toISOString().split('T')[0] : 'unknown',
|
|
281
|
+
daysSincePublish: daysSince,
|
|
282
|
+
maintainerStatus: maintainerStatus,
|
|
283
|
+
hasRepository: !!packageData.repository,
|
|
284
|
+
hasGithub: hasGithub,
|
|
285
|
+
totalVersions: Object.keys(packageData.versions || {}).length,
|
|
286
|
+
description: packageData.description || '',
|
|
287
|
+
deprecated: packageData.versions?.[latestVersion]?.deprecated || false
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Add GitHub metrics if available
|
|
291
|
+
if (github) {
|
|
292
|
+
result.githubMetrics = {
|
|
293
|
+
totalIssues: github.totalIssues,
|
|
294
|
+
recentIssues: github.last30Days,
|
|
295
|
+
trend: github.trend
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
results.push(result);
|
|
300
|
+
|
|
301
|
+
// Small delay to respect npm registry rate limits
|
|
302
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
303
|
+
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error(`Error analyzing ${packageName}:`, error.message);
|
|
306
|
+
// Continue with next package
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
results,
|
|
312
|
+
stats
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get quality recommendations for a package
|
|
318
|
+
*/
|
|
319
|
+
function getQualityRecommendation(packageResult) {
|
|
320
|
+
const { status, healthScore, daysSincePublish, maintainerStatus } = packageResult;
|
|
321
|
+
|
|
322
|
+
if (status === 'deprecated') {
|
|
323
|
+
return {
|
|
324
|
+
action: 'critical',
|
|
325
|
+
message: 'Package is deprecated',
|
|
326
|
+
recommendation: 'Find an actively maintained alternative immediately'
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (status === 'abandoned') {
|
|
331
|
+
return {
|
|
332
|
+
action: 'high',
|
|
333
|
+
message: `Last updated ${Math.floor(daysSincePublish / 365)} years ago`,
|
|
334
|
+
recommendation: 'Migrate to an actively maintained alternative'
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (status === 'stale') {
|
|
339
|
+
return {
|
|
340
|
+
action: 'medium',
|
|
341
|
+
message: `Not updated in ${Math.floor(daysSincePublish / 30)} months`,
|
|
342
|
+
recommendation: 'Consider finding a more actively maintained package'
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (status === 'needs_attention') {
|
|
347
|
+
return {
|
|
348
|
+
action: 'low',
|
|
349
|
+
message: `Health score: ${healthScore}/10`,
|
|
350
|
+
recommendation: 'Monitor for updates and potential alternatives'
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
action: 'none',
|
|
356
|
+
message: 'Package is healthy',
|
|
357
|
+
recommendation: 'No action needed'
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
module.exports = {
|
|
362
|
+
analyzePackageQuality,
|
|
363
|
+
calculateHealthScore,
|
|
364
|
+
getPackageStatus,
|
|
365
|
+
getMaintainerStatus,
|
|
366
|
+
getQualityRecommendation,
|
|
367
|
+
fetchNpmPackageInfo
|
|
368
|
+
};
|
|
@@ -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
|
+
};
|