licenseguard-cli 1.2.2 → 2.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 +2 -2
- package/README.md +355 -157
- package/bin/licenseguard.js +53 -34
- package/lib/commands/init-fast.js +62 -3
- package/lib/commands/init.js +74 -4
- package/lib/commands/scan.js +121 -0
- package/lib/scanner/compat-checker.js +433 -0
- package/lib/scanner/index.js +131 -0
- package/lib/scanner/license-compatibility-matrix.json +338 -0
- package/lib/scanner/license-detector.js +847 -0
- package/lib/scanner/license-normalizer.js +357 -0
- package/lib/scanner/plugins/cpp.js +267 -0
- package/lib/scanner/plugins/go.js +421 -0
- package/lib/scanner/plugins/node.js +149 -0
- package/lib/scanner/plugins/python-license-scanner.py +173 -0
- package/lib/scanner/plugins/python.js +336 -0
- package/lib/scanner/plugins/rust.js +196 -0
- package/lib/scanner/progress.js +22 -0
- package/lib/utils/file-ops.js +18 -1
- package/lib/utils/license-mapper.js +28 -0
- package/package.json +21 -5
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Go Ecosystem Plugin (Go Modules)
|
|
3
|
+
* Scans Go module dependencies for license information
|
|
4
|
+
*
|
|
5
|
+
* Features:
|
|
6
|
+
* - Dynamic module cache location via `go env GOMODCACHE` (AC #7)
|
|
7
|
+
* - Streaming NDJSON parsing via spawn for large projects (AC #8)
|
|
8
|
+
* - Jaccard Index license detection from LICENSE files (AC #6)
|
|
9
|
+
* - SPDX compatibility checking via compat-checker.js (AC #3)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs')
|
|
13
|
+
const path = require('path')
|
|
14
|
+
const { spawn, execSync } = require('child_process')
|
|
15
|
+
const { checkCompatibility } = require('../compat-checker')
|
|
16
|
+
const { showProgress } = require('../progress')
|
|
17
|
+
const { detectLicenseFromText } = require('../license-detector')
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Detect if this is a Go modules project
|
|
21
|
+
* @returns {boolean} True if go.mod exists
|
|
22
|
+
*/
|
|
23
|
+
function detect() {
|
|
24
|
+
return fs.existsSync('go.mod')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get Go module cache path dynamically via `go env GOMODCACHE`
|
|
29
|
+
* Does NOT hardcode $GOPATH/pkg/mod (AC #7)
|
|
30
|
+
*
|
|
31
|
+
* @returns {string} Module cache directory path
|
|
32
|
+
* @throws {Error} If Go is not installed
|
|
33
|
+
*/
|
|
34
|
+
function getGoModuleCachePath() {
|
|
35
|
+
try {
|
|
36
|
+
const cachePath = execSync('go env GOMODCACHE', {
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
timeout: 5000,
|
|
39
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
40
|
+
}).trim()
|
|
41
|
+
|
|
42
|
+
return cachePath
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error.message.includes('command not found') ||
|
|
45
|
+
error.message.includes('not recognized') ||
|
|
46
|
+
error.message.includes('ENOENT')) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'Go project detected but Go is not installed.\n\n' +
|
|
49
|
+
'Install Go: https://go.dev/doc/install\n' +
|
|
50
|
+
'Or skip scanning: licenseguard init --noscan'
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
throw new Error('Failed to get Go module cache path: ' + error.message)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get Go modules using streaming (spawn) for large projects (AC #8)
|
|
59
|
+
* Processes NDJSON output line-by-line to avoid maxBuffer limits
|
|
60
|
+
*
|
|
61
|
+
* @returns {Promise<Array>} Array of module objects from go list
|
|
62
|
+
* @throws {Error} If Go is not installed or command fails
|
|
63
|
+
*/
|
|
64
|
+
async function getGoModulesStreaming() {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const modules = []
|
|
67
|
+
let buffer = ''
|
|
68
|
+
let stderr = ''
|
|
69
|
+
|
|
70
|
+
const goList = spawn('go', ['list', '-m', '-json', 'all'], {
|
|
71
|
+
cwd: process.cwd(),
|
|
72
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Set timeout (30 seconds for large projects like Kubernetes)
|
|
76
|
+
const timeout = setTimeout(() => {
|
|
77
|
+
goList.kill('SIGTERM')
|
|
78
|
+
reject(new Error(
|
|
79
|
+
'Go module listing timed out after 30 seconds.\n\n' +
|
|
80
|
+
'This may indicate a very large project or network issues.\n' +
|
|
81
|
+
'Try running `go mod download` first to cache dependencies.'
|
|
82
|
+
))
|
|
83
|
+
}, 30000)
|
|
84
|
+
|
|
85
|
+
goList.stdout.on('data', (chunk) => {
|
|
86
|
+
buffer += chunk.toString()
|
|
87
|
+
|
|
88
|
+
// Process complete JSON objects (NDJSON format)
|
|
89
|
+
// Go outputs one JSON object per line, but objects may span multiple lines
|
|
90
|
+
// We need to parse complete JSON objects
|
|
91
|
+
let startIdx = 0
|
|
92
|
+
let braceCount = 0
|
|
93
|
+
let inString = false
|
|
94
|
+
let escape = false
|
|
95
|
+
|
|
96
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
97
|
+
const char = buffer[i]
|
|
98
|
+
|
|
99
|
+
if (escape) {
|
|
100
|
+
escape = false
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (char === '\\' && inString) {
|
|
105
|
+
escape = true
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (char === '"') {
|
|
110
|
+
inString = !inString
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (inString) continue
|
|
115
|
+
|
|
116
|
+
if (char === '{') {
|
|
117
|
+
if (braceCount === 0) startIdx = i
|
|
118
|
+
braceCount++
|
|
119
|
+
} else if (char === '}') {
|
|
120
|
+
braceCount--
|
|
121
|
+
if (braceCount === 0) {
|
|
122
|
+
// Complete JSON object found
|
|
123
|
+
const jsonStr = buffer.substring(startIdx, i + 1)
|
|
124
|
+
try {
|
|
125
|
+
const module = JSON.parse(jsonStr)
|
|
126
|
+
modules.push(module)
|
|
127
|
+
} catch (parseErr) {
|
|
128
|
+
// Skip malformed JSON
|
|
129
|
+
}
|
|
130
|
+
startIdx = i + 1
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Keep incomplete data in buffer
|
|
136
|
+
buffer = buffer.substring(startIdx)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
goList.stderr.on('data', (chunk) => {
|
|
140
|
+
stderr += chunk.toString()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
goList.on('error', (error) => {
|
|
144
|
+
clearTimeout(timeout)
|
|
145
|
+
if (error.code === 'ENOENT') {
|
|
146
|
+
reject(new Error(
|
|
147
|
+
'Go project detected but Go is not installed.\n\n' +
|
|
148
|
+
'Install Go: https://go.dev/doc/install\n' +
|
|
149
|
+
'Or skip scanning: licenseguard init --noscan'
|
|
150
|
+
))
|
|
151
|
+
} else {
|
|
152
|
+
reject(new Error('Failed to run go list: ' + error.message))
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
goList.on('close', (code) => {
|
|
157
|
+
clearTimeout(timeout)
|
|
158
|
+
|
|
159
|
+
if (code !== 0) {
|
|
160
|
+
// Check for common error patterns
|
|
161
|
+
if (stderr.includes('go.mod file not found')) {
|
|
162
|
+
reject(new Error(
|
|
163
|
+
'No go.mod found in current directory.\n\n' +
|
|
164
|
+
'Make sure you are in a Go modules project directory.'
|
|
165
|
+
))
|
|
166
|
+
} else if (stderr.includes('not a valid module')) {
|
|
167
|
+
reject(new Error('Invalid Go module: ' + stderr))
|
|
168
|
+
} else {
|
|
169
|
+
reject(new Error(`go list exited with code ${code}: ${stderr}`))
|
|
170
|
+
}
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
resolve(modules)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Synchronous fallback for getting Go modules (for smaller projects)
|
|
181
|
+
* @returns {Array} Array of module objects
|
|
182
|
+
*/
|
|
183
|
+
function getGoModulesSync() {
|
|
184
|
+
try {
|
|
185
|
+
const output = execSync('go list -m -json all', {
|
|
186
|
+
encoding: 'utf8',
|
|
187
|
+
timeout: 30000,
|
|
188
|
+
cwd: process.cwd(),
|
|
189
|
+
maxBuffer: 50 * 1024 * 1024, // 50MB buffer for large projects
|
|
190
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// Parse NDJSON (newline-delimited JSON)
|
|
194
|
+
const modules = []
|
|
195
|
+
let buffer = ''
|
|
196
|
+
let braceCount = 0
|
|
197
|
+
let startIdx = 0
|
|
198
|
+
let inString = false
|
|
199
|
+
let escape = false
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < output.length; i++) {
|
|
202
|
+
const char = output[i]
|
|
203
|
+
|
|
204
|
+
if (escape) {
|
|
205
|
+
escape = false
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (char === '\\' && inString) {
|
|
210
|
+
escape = true
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (char === '"') {
|
|
215
|
+
inString = !inString
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (inString) continue
|
|
220
|
+
|
|
221
|
+
if (char === '{') {
|
|
222
|
+
if (braceCount === 0) startIdx = i
|
|
223
|
+
braceCount++
|
|
224
|
+
} else if (char === '}') {
|
|
225
|
+
braceCount--
|
|
226
|
+
if (braceCount === 0) {
|
|
227
|
+
const jsonStr = output.substring(startIdx, i + 1)
|
|
228
|
+
try {
|
|
229
|
+
modules.push(JSON.parse(jsonStr))
|
|
230
|
+
} catch (parseErr) {
|
|
231
|
+
// Skip malformed JSON
|
|
232
|
+
}
|
|
233
|
+
startIdx = i + 1
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return modules
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (error.message.includes('command not found') ||
|
|
241
|
+
error.message.includes('not recognized') ||
|
|
242
|
+
error.message.includes('ENOENT')) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
'Go project detected but Go is not installed.\n\n' +
|
|
245
|
+
'Install Go: https://go.dev/doc/install\n' +
|
|
246
|
+
'Or skip scanning: licenseguard init --noscan'
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (error.killed) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
'Go module listing timed out.\n\n' +
|
|
253
|
+
'This may indicate a very large project.\n' +
|
|
254
|
+
'Try running `go mod download` first.'
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
throw new Error('Failed to get Go modules: ' + error.message)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Extract license from Go module in cache using Jaccard Index detection (AC #6)
|
|
264
|
+
*
|
|
265
|
+
* @param {Object} module - Module object with Path, Version, Dir
|
|
266
|
+
* @param {string} cachePath - Go module cache path
|
|
267
|
+
* @returns {string} SPDX license identifier or 'UNKNOWN'
|
|
268
|
+
*/
|
|
269
|
+
function extractGoLicense(module, cachePath) {
|
|
270
|
+
// Priority 1: Use Dir from go list output if available
|
|
271
|
+
let modulePath = module.Dir
|
|
272
|
+
|
|
273
|
+
// Priority 2: Construct path from cache + module path + version
|
|
274
|
+
if (!modulePath && cachePath && module.Path && module.Version) {
|
|
275
|
+
// Go module cache uses lowercase paths and @ for version separator
|
|
276
|
+
// e.g., /home/user/go/pkg/mod/github.com/gin-gonic/gin@v1.9.1
|
|
277
|
+
modulePath = path.join(cachePath, `${module.Path}@${module.Version}`)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!modulePath) {
|
|
281
|
+
return 'UNKNOWN'
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// License file names to check (in order of preference)
|
|
285
|
+
const licenseFiles = [
|
|
286
|
+
'LICENSE',
|
|
287
|
+
'LICENSE.txt',
|
|
288
|
+
'LICENSE.md',
|
|
289
|
+
'LICENCE', // British spelling
|
|
290
|
+
'LICENCE.txt',
|
|
291
|
+
'LICENCE.md',
|
|
292
|
+
'COPYING',
|
|
293
|
+
'COPYING.txt',
|
|
294
|
+
'MIT-LICENSE',
|
|
295
|
+
'MIT-LICENSE.txt',
|
|
296
|
+
'Apache-License-2.0.txt'
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
for (const filename of licenseFiles) {
|
|
300
|
+
const licensePath = path.join(modulePath, filename)
|
|
301
|
+
try {
|
|
302
|
+
if (fs.existsSync(licensePath)) {
|
|
303
|
+
const content = fs.readFileSync(licensePath, 'utf8')
|
|
304
|
+
// Use Jaccard Index algorithm from license-detector.js (AC #6)
|
|
305
|
+
const detected = detectLicenseFromText(content)
|
|
306
|
+
if (detected && detected !== 'UNKNOWN') {
|
|
307
|
+
return detected
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} catch (err) {
|
|
311
|
+
// Permission error or other issue - continue to next file
|
|
312
|
+
continue
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return 'UNKNOWN'
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Filter modules to only include actual dependencies (not the root module)
|
|
321
|
+
*
|
|
322
|
+
* @param {Array} modules - Array of module objects from go list
|
|
323
|
+
* @returns {Array} Filtered array of dependency modules
|
|
324
|
+
*/
|
|
325
|
+
function filterDependencies(modules) {
|
|
326
|
+
if (!modules || modules.length === 0) {
|
|
327
|
+
return []
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// The first module is typically the root module (the project itself)
|
|
331
|
+
// It has Main: true flag
|
|
332
|
+
return modules.filter(mod => {
|
|
333
|
+
// Skip the main/root module
|
|
334
|
+
if (mod.Main === true) {
|
|
335
|
+
return false
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Skip modules without Path (shouldn't happen, but safety check)
|
|
339
|
+
if (!mod.Path) {
|
|
340
|
+
return false
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return true
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Scan all Go module dependencies for license compatibility
|
|
349
|
+
*
|
|
350
|
+
* @param {string} projectLicense - The project's license (SPDX format)
|
|
351
|
+
* @returns {Promise<Object>} Scan results matching plugin interface
|
|
352
|
+
*/
|
|
353
|
+
async function scanDependencies(projectLicense) {
|
|
354
|
+
// Get module cache path dynamically (AC #7)
|
|
355
|
+
const cachePath = getGoModuleCachePath()
|
|
356
|
+
|
|
357
|
+
// Get modules using streaming for large projects (AC #8)
|
|
358
|
+
let allModules
|
|
359
|
+
try {
|
|
360
|
+
allModules = await getGoModulesStreaming()
|
|
361
|
+
} catch (streamErr) {
|
|
362
|
+
// Fallback to sync method for compatibility
|
|
363
|
+
allModules = getGoModulesSync()
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Filter to only actual dependencies
|
|
367
|
+
const modules = filterDependencies(allModules)
|
|
368
|
+
|
|
369
|
+
const results = {
|
|
370
|
+
timestamp: new Date().toISOString(),
|
|
371
|
+
totalDependencies: modules.length,
|
|
372
|
+
compatible: 0,
|
|
373
|
+
incompatible: 0,
|
|
374
|
+
unknown: 0,
|
|
375
|
+
issues: []
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Scan each dependency
|
|
379
|
+
for (let i = 0; i < modules.length; i++) {
|
|
380
|
+
showProgress(i + 1, modules.length)
|
|
381
|
+
|
|
382
|
+
const mod = modules[i]
|
|
383
|
+
const license = extractGoLicense(mod, cachePath)
|
|
384
|
+
|
|
385
|
+
// Check compatibility using universal compat-checker
|
|
386
|
+
const compatResult = checkCompatibility(projectLicense, license)
|
|
387
|
+
|
|
388
|
+
if (!compatResult.compatible) {
|
|
389
|
+
const isUnknown = license === 'UNKNOWN'
|
|
390
|
+
|
|
391
|
+
if (isUnknown) {
|
|
392
|
+
results.unknown++
|
|
393
|
+
} else {
|
|
394
|
+
results.incompatible++
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
results.issues.push({
|
|
398
|
+
package: `${mod.Path}@${mod.Version}`,
|
|
399
|
+
license: license,
|
|
400
|
+
type: isUnknown ? 'warning' : 'conflict',
|
|
401
|
+
reason: compatResult.reason,
|
|
402
|
+
location: mod.Dir || `${cachePath}/${mod.Path}@${mod.Version}`
|
|
403
|
+
})
|
|
404
|
+
} else {
|
|
405
|
+
results.compatible++
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return results
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
module.exports = {
|
|
413
|
+
detect,
|
|
414
|
+
scanDependencies,
|
|
415
|
+
// Export internal functions for testing
|
|
416
|
+
getGoModuleCachePath,
|
|
417
|
+
getGoModulesStreaming,
|
|
418
|
+
getGoModulesSync,
|
|
419
|
+
extractGoLicense,
|
|
420
|
+
filterDependencies
|
|
421
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js Ecosystem Plugin
|
|
3
|
+
* Scans node_modules for license information
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs')
|
|
7
|
+
const path = require('path')
|
|
8
|
+
const { checkCompatibility } = require('../compat-checker')
|
|
9
|
+
const { showProgress } = require('../progress')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Detect if this is a Node.js project
|
|
13
|
+
* @returns {boolean} True if package.json exists
|
|
14
|
+
*/
|
|
15
|
+
function detect() {
|
|
16
|
+
return fs.existsSync('package.json')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parse package.json to get dependencies
|
|
21
|
+
* @returns {{deps: string[], packageJson: Object}} Dependency list and package.json
|
|
22
|
+
*/
|
|
23
|
+
function parsePackageJson() {
|
|
24
|
+
try {
|
|
25
|
+
const content = fs.readFileSync('package.json', 'utf8')
|
|
26
|
+
const packageJson = JSON.parse(content)
|
|
27
|
+
|
|
28
|
+
// Only scan production dependencies
|
|
29
|
+
const deps = Object.keys(packageJson.dependencies || {})
|
|
30
|
+
|
|
31
|
+
return { deps, packageJson }
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new Error('Failed to read package.json: ' + error.message)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract license info from a dependency
|
|
39
|
+
* @param {string} depName - Dependency name
|
|
40
|
+
* @returns {{name: string, version: string, license: string, path: string}} License info
|
|
41
|
+
*/
|
|
42
|
+
function extractLicense(depName) {
|
|
43
|
+
const depPath = path.join('node_modules', depName, 'package.json')
|
|
44
|
+
|
|
45
|
+
if (!fs.existsSync(depPath)) {
|
|
46
|
+
return {
|
|
47
|
+
name: depName,
|
|
48
|
+
version: 'unknown',
|
|
49
|
+
license: 'NOT_INSTALLED',
|
|
50
|
+
path: depPath
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const content = fs.readFileSync(depPath, 'utf8')
|
|
56
|
+
const depPackageJson = JSON.parse(content)
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
name: depName,
|
|
60
|
+
version: depPackageJson.version || 'unknown',
|
|
61
|
+
license: depPackageJson.license || 'UNKNOWN',
|
|
62
|
+
path: depPath
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return {
|
|
66
|
+
name: depName,
|
|
67
|
+
version: 'unknown',
|
|
68
|
+
license: 'PARSE_ERROR',
|
|
69
|
+
path: depPath
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Scan all Node.js dependencies for license compatibility
|
|
76
|
+
* @param {string} projectLicense - The project's license
|
|
77
|
+
* @returns {Promise<Object>} Scan results
|
|
78
|
+
*/
|
|
79
|
+
async function scanDependencies(projectLicense) {
|
|
80
|
+
// 1. Read project package.json
|
|
81
|
+
const { deps } = parsePackageJson()
|
|
82
|
+
|
|
83
|
+
const results = {
|
|
84
|
+
timestamp: new Date().toISOString(),
|
|
85
|
+
totalDependencies: deps.length,
|
|
86
|
+
compatible: 0,
|
|
87
|
+
incompatible: 0,
|
|
88
|
+
unknown: 0,
|
|
89
|
+
issues: []
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 2. Scan each dependency
|
|
93
|
+
for (let i = 0; i < deps.length; i++) {
|
|
94
|
+
showProgress(i + 1, deps.length)
|
|
95
|
+
|
|
96
|
+
const depName = deps[i]
|
|
97
|
+
const depInfo = extractLicense(depName)
|
|
98
|
+
|
|
99
|
+
// Skip if not installed
|
|
100
|
+
if (depInfo.license === 'NOT_INSTALLED') {
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Handle parse errors as unknown
|
|
105
|
+
if (depInfo.license === 'PARSE_ERROR') {
|
|
106
|
+
results.unknown++
|
|
107
|
+
results.issues.push({
|
|
108
|
+
package: `${depName}@${depInfo.version}`,
|
|
109
|
+
license: 'UNKNOWN',
|
|
110
|
+
type: 'warning',
|
|
111
|
+
reason: 'Failed to parse package.json',
|
|
112
|
+
location: depInfo.path
|
|
113
|
+
})
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 3. Check compatibility
|
|
118
|
+
const compatResult = checkCompatibility(projectLicense, depInfo.license)
|
|
119
|
+
|
|
120
|
+
if (!compatResult.compatible) {
|
|
121
|
+
const isUnknown = depInfo.license === 'UNKNOWN'
|
|
122
|
+
|
|
123
|
+
if (isUnknown) {
|
|
124
|
+
results.unknown++
|
|
125
|
+
} else {
|
|
126
|
+
results.incompatible++
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
results.issues.push({
|
|
130
|
+
package: `${depName}@${depInfo.version}`,
|
|
131
|
+
license: depInfo.license,
|
|
132
|
+
type: isUnknown ? 'warning' : 'conflict',
|
|
133
|
+
reason: compatResult.reason,
|
|
134
|
+
location: depInfo.path
|
|
135
|
+
})
|
|
136
|
+
} else {
|
|
137
|
+
results.compatible++
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return results
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
detect,
|
|
146
|
+
scanDependencies,
|
|
147
|
+
parsePackageJson,
|
|
148
|
+
extractLicense
|
|
149
|
+
}
|