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,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"malicious_packages": [
|
|
3
|
+
"epress",
|
|
4
|
+
"expres",
|
|
5
|
+
"expresss",
|
|
6
|
+
"reqest",
|
|
7
|
+
"requet",
|
|
8
|
+
"lodas",
|
|
9
|
+
"loadsh",
|
|
10
|
+
"axois",
|
|
11
|
+
"axioss",
|
|
12
|
+
"webpak",
|
|
13
|
+
"webpackk",
|
|
14
|
+
"reactt",
|
|
15
|
+
"vuee",
|
|
16
|
+
"angularr"
|
|
17
|
+
],
|
|
18
|
+
"typosquat_patterns": {
|
|
19
|
+
"express": ["epress", "expres", "expresss", "exprss"],
|
|
20
|
+
"request": ["reqest", "requet", "requets"],
|
|
21
|
+
"lodash": ["lodas", "loadsh", "lodahs", "lodsh"],
|
|
22
|
+
"axios": ["axois", "axioss", "axos", "axious"],
|
|
23
|
+
"webpack": ["webpak", "webpackk", "wepback"],
|
|
24
|
+
"react": ["reactt", "reakt", "raect"],
|
|
25
|
+
"vue": ["vuee", "veu", "vuw"],
|
|
26
|
+
"angular": ["angularr", "anguler", "angulr"],
|
|
27
|
+
"next": ["nextt", "nxt", "nex"],
|
|
28
|
+
"typescript": ["typscript", "typescrpt", "typescrip"],
|
|
29
|
+
"eslint": ["esslint", "elint", "eslnt"],
|
|
30
|
+
"prettier": ["pretier", "prettir", "pretter"],
|
|
31
|
+
"jest": ["jst", "jestt", "jест"],
|
|
32
|
+
"mocha": ["mocha", "mоcha", "mосha"],
|
|
33
|
+
"chai": ["chаi", "сhai", "chаi"]
|
|
34
|
+
},
|
|
35
|
+
"suspicious_patterns": {
|
|
36
|
+
"install_scripts": [
|
|
37
|
+
"curl",
|
|
38
|
+
"wget",
|
|
39
|
+
"powershell",
|
|
40
|
+
"eval",
|
|
41
|
+
"exec",
|
|
42
|
+
"child_process",
|
|
43
|
+
"/bin/sh",
|
|
44
|
+
"/bin/bash",
|
|
45
|
+
"http://",
|
|
46
|
+
"https://"
|
|
47
|
+
],
|
|
48
|
+
"suspicious_dependencies": [
|
|
49
|
+
"bitcoin",
|
|
50
|
+
"cryptocurrency",
|
|
51
|
+
"mining",
|
|
52
|
+
"miner",
|
|
53
|
+
"keylogger",
|
|
54
|
+
"backdoor"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devcompass",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Dependency health checker with ecosystem intelligence
|
|
3
|
+
"version": "2.7.1",
|
|
4
|
+
"description": "Dependency health checker with ecosystem intelligence, real-time GitHub issue tracking for 500+ popular npm packages, parallel processing, supply chain security analysis, and advanced license risk detection.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"devcompass": "./bin/devcompass.js"
|
|
@@ -45,7 +45,15 @@
|
|
|
45
45
|
"package-health",
|
|
46
46
|
"top-500-packages",
|
|
47
47
|
"parallel-processing",
|
|
48
|
-
"performance-optimization"
|
|
48
|
+
"performance-optimization",
|
|
49
|
+
"supply-chain-security",
|
|
50
|
+
"typosquatting-detection",
|
|
51
|
+
"malicious-packages",
|
|
52
|
+
"license-compliance",
|
|
53
|
+
"license-risk",
|
|
54
|
+
"package-quality",
|
|
55
|
+
"security-recommendations",
|
|
56
|
+
"dependency-quality"
|
|
49
57
|
],
|
|
50
58
|
"author": "Ajay Thorat <ajaythorat988@gmail.com>",
|
|
51
59
|
"license": "MIT",
|
|
@@ -68,4 +76,4 @@
|
|
|
68
76
|
"url": "https://github.com/AjayBThorat-20/devcompass/issues"
|
|
69
77
|
},
|
|
70
78
|
"homepage": "https://github.com/AjayBThorat-20/devcompass#readme"
|
|
71
|
-
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// src/analyzers/license-risk.js
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* License risk levels and compatibility
|
|
7
|
+
*/
|
|
8
|
+
const LICENSE_RISKS = {
|
|
9
|
+
// High Risk - Restrictive/Copyleft
|
|
10
|
+
'GPL-1.0': { risk: 'high', type: 'copyleft', business: 'Requires source disclosure' },
|
|
11
|
+
'GPL-2.0': { risk: 'high', type: 'copyleft', business: 'Requires source disclosure' },
|
|
12
|
+
'GPL-3.0': { risk: 'high', type: 'copyleft', business: 'Requires source disclosure' },
|
|
13
|
+
'AGPL-1.0': { risk: 'critical', type: 'copyleft', business: 'Network copyleft - very restrictive' },
|
|
14
|
+
'AGPL-3.0': { risk: 'critical', type: 'copyleft', business: 'Network copyleft - very restrictive' },
|
|
15
|
+
'LGPL-2.0': { risk: 'medium', type: 'weak-copyleft', business: 'Limited copyleft obligations' },
|
|
16
|
+
'LGPL-2.1': { risk: 'medium', type: 'weak-copyleft', business: 'Limited copyleft obligations' },
|
|
17
|
+
'LGPL-3.0': { risk: 'medium', type: 'weak-copyleft', business: 'Limited copyleft obligations' },
|
|
18
|
+
|
|
19
|
+
// Medium Risk
|
|
20
|
+
'MPL-1.0': { risk: 'medium', type: 'weak-copyleft', business: 'File-level copyleft' },
|
|
21
|
+
'MPL-1.1': { risk: 'medium', type: 'weak-copyleft', business: 'File-level copyleft' },
|
|
22
|
+
'MPL-2.0': { risk: 'medium', type: 'weak-copyleft', business: 'File-level copyleft' },
|
|
23
|
+
'EPL-1.0': { risk: 'medium', type: 'weak-copyleft', business: 'Module-level copyleft' },
|
|
24
|
+
'EPL-2.0': { risk: 'medium', type: 'weak-copyleft', business: 'Module-level copyleft' },
|
|
25
|
+
'CDDL-1.0': { risk: 'medium', type: 'weak-copyleft', business: 'File-level copyleft' },
|
|
26
|
+
|
|
27
|
+
// Low Risk - Permissive
|
|
28
|
+
'MIT': { risk: 'low', type: 'permissive', business: 'Very permissive' },
|
|
29
|
+
'Apache-2.0': { risk: 'low', type: 'permissive', business: 'Permissive with patent grant' },
|
|
30
|
+
'BSD-2-Clause': { risk: 'low', type: 'permissive', business: 'Very permissive' },
|
|
31
|
+
'BSD-3-Clause': { risk: 'low', type: 'permissive', business: 'Very permissive' },
|
|
32
|
+
'ISC': { risk: 'low', type: 'permissive', business: 'Very permissive' },
|
|
33
|
+
'CC0-1.0': { risk: 'low', type: 'public-domain', business: 'Public domain' },
|
|
34
|
+
'Unlicense': { risk: 'low', type: 'public-domain', business: 'Public domain' },
|
|
35
|
+
'0BSD': { risk: 'low', type: 'permissive', business: 'Very permissive' },
|
|
36
|
+
|
|
37
|
+
// Unknown/Special
|
|
38
|
+
'UNLICENSED': { risk: 'critical', type: 'unknown', business: 'No license - all rights reserved' },
|
|
39
|
+
'SEE LICENSE IN': { risk: 'high', type: 'unknown', business: 'Custom license - review required' },
|
|
40
|
+
'CUSTOM': { risk: 'high', type: 'unknown', business: 'Custom license - review required' }
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* License compatibility matrix
|
|
45
|
+
* Can license A be combined with license B?
|
|
46
|
+
*/
|
|
47
|
+
const LICENSE_COMPATIBILITY = {
|
|
48
|
+
'MIT': ['MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'GPL-2.0', 'GPL-3.0', 'LGPL-2.1', 'LGPL-3.0'],
|
|
49
|
+
'Apache-2.0': ['Apache-2.0', 'GPL-3.0', 'LGPL-3.0'],
|
|
50
|
+
'GPL-2.0': ['GPL-2.0', 'MIT', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC'],
|
|
51
|
+
'GPL-3.0': ['GPL-3.0', 'MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC'],
|
|
52
|
+
'LGPL-2.1': ['LGPL-2.1', 'MIT', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'GPL-2.0'],
|
|
53
|
+
'LGPL-3.0': ['LGPL-3.0', 'MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'GPL-3.0']
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalize license name
|
|
58
|
+
*/
|
|
59
|
+
function normalizeLicense(license) {
|
|
60
|
+
if (!license) return 'UNLICENSED';
|
|
61
|
+
|
|
62
|
+
const normalized = license
|
|
63
|
+
.replace(/\s+/g, '-')
|
|
64
|
+
.replace(/[()]/g, '')
|
|
65
|
+
.toUpperCase();
|
|
66
|
+
|
|
67
|
+
// Handle common variations
|
|
68
|
+
if (normalized.includes('MIT')) return 'MIT';
|
|
69
|
+
if (normalized.includes('APACHE-2')) return 'Apache-2.0';
|
|
70
|
+
if (normalized.includes('BSD-2')) return 'BSD-2-Clause';
|
|
71
|
+
if (normalized.includes('BSD-3')) return 'BSD-3-Clause';
|
|
72
|
+
if (normalized.includes('ISC')) return 'ISC';
|
|
73
|
+
if (normalized.includes('GPL-2')) return 'GPL-2.0';
|
|
74
|
+
if (normalized.includes('GPL-3')) return 'GPL-3.0';
|
|
75
|
+
if (normalized.includes('LGPL-2')) return 'LGPL-2.1';
|
|
76
|
+
if (normalized.includes('LGPL-3')) return 'LGPL-3.0';
|
|
77
|
+
if (normalized.includes('AGPL')) return 'AGPL-3.0';
|
|
78
|
+
if (normalized.includes('MPL')) return 'MPL-2.0';
|
|
79
|
+
if (normalized.includes('SEE LICENSE')) return 'SEE LICENSE IN';
|
|
80
|
+
if (normalized === 'UNLICENSED') return 'UNLICENSED';
|
|
81
|
+
|
|
82
|
+
return license;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get license risk information
|
|
87
|
+
*/
|
|
88
|
+
function getLicenseRisk(license) {
|
|
89
|
+
const normalized = normalizeLicense(license);
|
|
90
|
+
return LICENSE_RISKS[normalized] || {
|
|
91
|
+
risk: 'high',
|
|
92
|
+
type: 'unknown',
|
|
93
|
+
business: 'Unknown license - review required'
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check license compatibility
|
|
99
|
+
*/
|
|
100
|
+
function checkLicenseCompatibility(projectLicense, dependencyLicenses) {
|
|
101
|
+
const conflicts = [];
|
|
102
|
+
const normalized = normalizeLicense(projectLicense);
|
|
103
|
+
const compatible = LICENSE_COMPATIBILITY[normalized] || [];
|
|
104
|
+
|
|
105
|
+
for (const [pkg, license] of Object.entries(dependencyLicenses)) {
|
|
106
|
+
const depNormalized = normalizeLicense(license);
|
|
107
|
+
const depRisk = getLicenseRisk(license);
|
|
108
|
+
|
|
109
|
+
// Check if copyleft license conflicts with permissive project
|
|
110
|
+
if (depRisk.type === 'copyleft' && !compatible.includes(depNormalized)) {
|
|
111
|
+
conflicts.push({
|
|
112
|
+
package: pkg,
|
|
113
|
+
license: license,
|
|
114
|
+
projectLicense: projectLicense,
|
|
115
|
+
severity: 'high',
|
|
116
|
+
issue: 'License incompatibility',
|
|
117
|
+
message: `${license} dependency may conflict with ${projectLicense} project license`,
|
|
118
|
+
recommendation: 'Review license compatibility with legal team'
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return conflicts;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Analyze license risks for all dependencies
|
|
128
|
+
*/
|
|
129
|
+
async function analyzeLicenseRisks(projectPath, licenses) {
|
|
130
|
+
const warnings = [];
|
|
131
|
+
const stats = {
|
|
132
|
+
total: 0,
|
|
133
|
+
critical: 0,
|
|
134
|
+
high: 0,
|
|
135
|
+
medium: 0,
|
|
136
|
+
low: 0,
|
|
137
|
+
copyleft: 0,
|
|
138
|
+
permissive: 0,
|
|
139
|
+
unknown: 0
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Get project license
|
|
143
|
+
let projectLicense = 'MIT'; // Default
|
|
144
|
+
try {
|
|
145
|
+
const projectPkgPath = path.join(projectPath, 'package.json');
|
|
146
|
+
if (fs.existsSync(projectPkgPath)) {
|
|
147
|
+
const projectPkg = JSON.parse(fs.readFileSync(projectPkgPath, 'utf8'));
|
|
148
|
+
projectLicense = projectPkg.license || 'MIT';
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
// Use default
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const dependencyLicenses = {};
|
|
155
|
+
|
|
156
|
+
// Analyze each license
|
|
157
|
+
for (const pkg of licenses) {
|
|
158
|
+
stats.total++;
|
|
159
|
+
|
|
160
|
+
const risk = getLicenseRisk(pkg.license);
|
|
161
|
+
dependencyLicenses[pkg.package] = pkg.license;
|
|
162
|
+
|
|
163
|
+
// Count by type
|
|
164
|
+
if (risk.type === 'copyleft' || risk.type === 'weak-copyleft') {
|
|
165
|
+
stats.copyleft++;
|
|
166
|
+
} else if (risk.type === 'permissive' || risk.type === 'public-domain') {
|
|
167
|
+
stats.permissive++;
|
|
168
|
+
} else {
|
|
169
|
+
stats.unknown++;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Add warnings for high-risk licenses
|
|
173
|
+
if (risk.risk === 'critical' || risk.risk === 'high') {
|
|
174
|
+
stats[risk.risk]++;
|
|
175
|
+
|
|
176
|
+
warnings.push({
|
|
177
|
+
package: pkg.package,
|
|
178
|
+
license: pkg.license,
|
|
179
|
+
severity: risk.risk,
|
|
180
|
+
type: risk.type,
|
|
181
|
+
issue: 'High-risk license',
|
|
182
|
+
message: `${pkg.license}: ${risk.business}`,
|
|
183
|
+
recommendation: risk.risk === 'critical'
|
|
184
|
+
? 'Replace with permissive alternative immediately'
|
|
185
|
+
: 'Consider replacing with MIT/Apache alternative'
|
|
186
|
+
});
|
|
187
|
+
} else if (risk.risk === 'medium') {
|
|
188
|
+
stats.medium++;
|
|
189
|
+
} else {
|
|
190
|
+
stats.low++;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check license compatibility
|
|
195
|
+
const conflicts = checkLicenseCompatibility(projectLicense, dependencyLicenses);
|
|
196
|
+
warnings.push(...conflicts);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
warnings,
|
|
200
|
+
stats,
|
|
201
|
+
projectLicense
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get license risk score (0-10)
|
|
207
|
+
*/
|
|
208
|
+
function getLicenseRiskScore(stats) {
|
|
209
|
+
let score = 10;
|
|
210
|
+
|
|
211
|
+
score -= stats.critical * 3;
|
|
212
|
+
score -= stats.high * 2;
|
|
213
|
+
score -= stats.medium * 0.5;
|
|
214
|
+
|
|
215
|
+
return Math.max(0, score);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = {
|
|
219
|
+
analyzeLicenseRisks,
|
|
220
|
+
getLicenseRisk,
|
|
221
|
+
checkLicenseCompatibility,
|
|
222
|
+
normalizeLicense,
|
|
223
|
+
getLicenseRiskScore,
|
|
224
|
+
LICENSE_RISKS
|
|
225
|
+
};
|
|
@@ -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
|
+
};
|