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.
@@ -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
+ };