licenseguard-cli 2.0.0 → 2.1.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/CHANGELOG.md +210 -0
- package/README.md +111 -9
- package/bin/licenseguard.js +26 -0
- package/lib/commands/init-fast.js +2 -10
- package/lib/commands/init.js +3 -11
- package/lib/commands/scan.js +122 -0
- package/lib/scanner/color-mapper.js +87 -0
- package/lib/scanner/compat-checker.js +369 -50
- package/lib/scanner/index.js +75 -117
- 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 +420 -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/utils/license-mapper.js +28 -0
- package/lib/utils/update-notifier.js +141 -0
- package/package.json +2 -2
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* License Normalizer
|
|
3
|
+
* Normalizes license identifiers to canonical SPDX form
|
|
4
|
+
* Handles ecosystem-specific variants (Python, Rust, C++, npm)
|
|
5
|
+
*
|
|
6
|
+
* This centralizes license normalization logic that was previously scattered
|
|
7
|
+
* across ecosystem plugins, making it reusable and consistent.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Common license format variations → Canonical SPDX identifiers
|
|
12
|
+
* Covers Python, Rust, C++, and npm ecosystem quirks
|
|
13
|
+
*
|
|
14
|
+
* Sources:
|
|
15
|
+
* - Python: Trove Classifiers + pip show metadata
|
|
16
|
+
* - Rust: Cargo.toml license field patterns
|
|
17
|
+
* - C++: Conan license field + package.json variants
|
|
18
|
+
* - npm: package.json license field
|
|
19
|
+
*/
|
|
20
|
+
const LICENSE_NORMALIZATIONS = {
|
|
21
|
+
// ============================================
|
|
22
|
+
// Apache variants
|
|
23
|
+
// ============================================
|
|
24
|
+
'Apache 2.0': 'Apache-2.0',
|
|
25
|
+
'Apache License 2.0': 'Apache-2.0',
|
|
26
|
+
'Apache License, Version 2.0': 'Apache-2.0',
|
|
27
|
+
'Apache Software License': 'Apache-2.0', // Python Trove Classifier
|
|
28
|
+
'ASL 2': 'Apache-2.0',
|
|
29
|
+
'Apache-2': 'Apache-2.0', // Rust/C++ shorthand
|
|
30
|
+
|
|
31
|
+
// ============================================
|
|
32
|
+
// BSD variants (most common in Python/C++)
|
|
33
|
+
// ============================================
|
|
34
|
+
'BSD License': 'BSD-3-Clause', // Default to 3-Clause (most common)
|
|
35
|
+
'BSD': 'BSD-3-Clause',
|
|
36
|
+
'BSD 3-Clause': 'BSD-3-Clause',
|
|
37
|
+
'BSD 3 Clause': 'BSD-3-Clause',
|
|
38
|
+
'3-Clause BSD License': 'BSD-3-Clause',
|
|
39
|
+
'New BSD': 'BSD-3-Clause',
|
|
40
|
+
'Modified BSD': 'BSD-3-Clause',
|
|
41
|
+
'BSD 2-Clause': 'BSD-2-Clause',
|
|
42
|
+
'Simplified BSD': 'BSD-2-Clause',
|
|
43
|
+
'FreeBSD': 'BSD-2-Clause',
|
|
44
|
+
|
|
45
|
+
// ============================================
|
|
46
|
+
// MIT variants
|
|
47
|
+
// ============================================
|
|
48
|
+
'MIT': 'MIT',
|
|
49
|
+
'MIT License': 'MIT',
|
|
50
|
+
'mit': 'MIT',
|
|
51
|
+
'Mit': 'MIT',
|
|
52
|
+
|
|
53
|
+
// ============================================
|
|
54
|
+
// GPL variants (SPDX 3.0 normalization)
|
|
55
|
+
// ============================================
|
|
56
|
+
// CRITICAL: GPL-3.0 is deprecated in SPDX 3.0 → GPL-3.0-only
|
|
57
|
+
'GPL-3.0': 'GPL-3.0-only',
|
|
58
|
+
'GPL 3.0': 'GPL-3.0-only',
|
|
59
|
+
'GPLv3': 'GPL-3.0-only',
|
|
60
|
+
'GPL-3.0.0': 'GPL-3.0-only',
|
|
61
|
+
'GNU General Public License v3.0': 'GPL-3.0-only',
|
|
62
|
+
'GNU General Public License (GPL)': 'GPL-3.0-only', // Default to v3
|
|
63
|
+
|
|
64
|
+
'GPL-2.0': 'GPL-2.0-only',
|
|
65
|
+
'GPL 2.0': 'GPL-2.0-only',
|
|
66
|
+
'GPLv2': 'GPL-2.0-only',
|
|
67
|
+
'GPL-2.0.0': 'GPL-2.0-only',
|
|
68
|
+
'GNU General Public License v2.0': 'GPL-2.0-only',
|
|
69
|
+
|
|
70
|
+
// GPL "or-later" variants
|
|
71
|
+
'GPL-3.0+': 'GPL-3.0-or-later',
|
|
72
|
+
'GPL-3.0-or-later': 'GPL-3.0-or-later',
|
|
73
|
+
'GPLv3+': 'GPL-3.0-or-later',
|
|
74
|
+
|
|
75
|
+
'GPL-2.0+': 'GPL-2.0-or-later',
|
|
76
|
+
'GPL-2.0-or-later': 'GPL-2.0-or-later',
|
|
77
|
+
'GPLv2+': 'GPL-2.0-or-later',
|
|
78
|
+
|
|
79
|
+
// ============================================
|
|
80
|
+
// LGPL variants
|
|
81
|
+
// ============================================
|
|
82
|
+
'GNU Library or Lesser General Public License (LGPL)': 'LGPL-2.1-or-later',
|
|
83
|
+
'LGPL': 'LGPL-2.1-or-later', // Default to 2.1-or-later (most permissive)
|
|
84
|
+
'LGPLv3': 'LGPL-3.0-only',
|
|
85
|
+
'LGPL-3.0': 'LGPL-3.0-only',
|
|
86
|
+
'LGPL-3.0-only': 'LGPL-3.0-only',
|
|
87
|
+
'LGPL-3.0-or-later': 'LGPL-3.0-or-later',
|
|
88
|
+
'LGPLv2': 'LGPL-2.1-only',
|
|
89
|
+
'LGPL-2.1': 'LGPL-2.1-only',
|
|
90
|
+
'LGPL-2.1-only': 'LGPL-2.1-only',
|
|
91
|
+
'LGPL-2.1-or-later': 'LGPL-2.1-or-later',
|
|
92
|
+
'LGPL-2.0-only': 'LGPL-2.0-only',
|
|
93
|
+
'LGPL-2.0-or-later': 'LGPL-2.0-or-later',
|
|
94
|
+
|
|
95
|
+
// ============================================
|
|
96
|
+
// ISC variants
|
|
97
|
+
// ============================================
|
|
98
|
+
'ISC': 'ISC',
|
|
99
|
+
'ISC License': 'ISC',
|
|
100
|
+
'ISC License (ISCL)': 'ISC',
|
|
101
|
+
|
|
102
|
+
// ============================================
|
|
103
|
+
// MPL variants
|
|
104
|
+
// ============================================
|
|
105
|
+
'MPL-2.0': 'MPL-2.0',
|
|
106
|
+
'Mozilla Public License 2.0': 'MPL-2.0',
|
|
107
|
+
'Mozilla Public License 2.0 (MPL 2.0)': 'MPL-2.0',
|
|
108
|
+
'MPL 2.0': 'MPL-2.0',
|
|
109
|
+
|
|
110
|
+
// ============================================
|
|
111
|
+
// Python Software Foundation
|
|
112
|
+
// ============================================
|
|
113
|
+
'Python Software Foundation License': 'PSF-2.0',
|
|
114
|
+
'PSF': 'PSF-2.0',
|
|
115
|
+
|
|
116
|
+
// ============================================
|
|
117
|
+
// Public Domain / Ultra-Permissive
|
|
118
|
+
// ============================================
|
|
119
|
+
'Unlicense': 'Unlicense',
|
|
120
|
+
'Public Domain': 'Unlicense', // Map to Unlicense (closest SPDX)
|
|
121
|
+
'CC0-1.0': 'CC0-1.0',
|
|
122
|
+
'CC0': 'CC0-1.0',
|
|
123
|
+
'WTFPL': 'WTFPL',
|
|
124
|
+
'0BSD': '0BSD',
|
|
125
|
+
|
|
126
|
+
// ============================================
|
|
127
|
+
// Zlib / bzip2 (permissive)
|
|
128
|
+
// ============================================
|
|
129
|
+
'Zlib': 'Zlib',
|
|
130
|
+
'zlib License': 'Zlib',
|
|
131
|
+
'bzip2-1.0.6': 'bzip2-1.0.6',
|
|
132
|
+
'bzip2': 'bzip2-1.0.6',
|
|
133
|
+
|
|
134
|
+
// ============================================
|
|
135
|
+
// Boost Software License
|
|
136
|
+
// ============================================
|
|
137
|
+
'BSL-1.0': 'BSL-1.0',
|
|
138
|
+
'Boost Software License 1.0': 'BSL-1.0',
|
|
139
|
+
'Boost': 'BSL-1.0',
|
|
140
|
+
|
|
141
|
+
// ============================================
|
|
142
|
+
// Proprietary / Non-SPDX
|
|
143
|
+
// ============================================
|
|
144
|
+
'Other/Proprietary License': 'LicenseRef-Proprietary',
|
|
145
|
+
'NVIDIA Proprietary Software': 'LicenseRef-NVIDIA-Proprietary',
|
|
146
|
+
'Proprietary': 'LicenseRef-Proprietary',
|
|
147
|
+
'Commercial': 'LicenseRef-Commercial'
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* License family classifications
|
|
152
|
+
* Used for upgrade path detection (e.g., LGPL can upgrade to GPL)
|
|
153
|
+
*/
|
|
154
|
+
const LICENSE_FAMILIES = {
|
|
155
|
+
// GPL Family (Strong Copyleft)
|
|
156
|
+
'GPL-2.0-only': 'GPL2',
|
|
157
|
+
'GPL-2.0-or-later': 'GPL2-or-later',
|
|
158
|
+
'GPL-3.0-only': 'GPL3',
|
|
159
|
+
'GPL-3.0-or-later': 'GPL3-or-later',
|
|
160
|
+
|
|
161
|
+
// LGPL Family (Weak Copyleft - can upgrade to GPL)
|
|
162
|
+
'LGPL-2.1-only': 'LGPL',
|
|
163
|
+
'LGPL-2.1-or-later': 'LGPL-or-later',
|
|
164
|
+
'LGPL-3.0-only': 'LGPL3',
|
|
165
|
+
'LGPL-3.0-or-later': 'LGPL3-or-later',
|
|
166
|
+
'LGPL-2.0-only': 'LGPL',
|
|
167
|
+
'LGPL-2.0-or-later': 'LGPL-or-later',
|
|
168
|
+
|
|
169
|
+
// AGPL Family (Network Copyleft)
|
|
170
|
+
'AGPL-3.0-only': 'AGPL3',
|
|
171
|
+
'AGPL-3.0-or-later': 'AGPL3-or-later',
|
|
172
|
+
|
|
173
|
+
// MPL Family (Weak Copyleft - Section 3.3 allows GPL combination)
|
|
174
|
+
'MPL-2.0': 'MPL',
|
|
175
|
+
'MPL-1.1': 'MPL',
|
|
176
|
+
|
|
177
|
+
// Apache Family (Permissive with Patent Grant)
|
|
178
|
+
'Apache-2.0': 'Apache',
|
|
179
|
+
'Apache-1.1': 'Apache',
|
|
180
|
+
|
|
181
|
+
// MIT Family (Permissive)
|
|
182
|
+
'MIT': 'MIT',
|
|
183
|
+
|
|
184
|
+
// BSD Family (Permissive)
|
|
185
|
+
'BSD-3-Clause': 'BSD',
|
|
186
|
+
'BSD-2-Clause': 'BSD',
|
|
187
|
+
'BSD-1-Clause': 'BSD',
|
|
188
|
+
'0BSD': 'BSD',
|
|
189
|
+
|
|
190
|
+
// ISC Family (Permissive)
|
|
191
|
+
'ISC': 'ISC',
|
|
192
|
+
|
|
193
|
+
// Public Domain
|
|
194
|
+
'Unlicense': 'Public-Domain',
|
|
195
|
+
'CC0-1.0': 'Public-Domain',
|
|
196
|
+
'WTFPL': 'Public-Domain',
|
|
197
|
+
|
|
198
|
+
// Other Permissive
|
|
199
|
+
'Zlib': 'Permissive',
|
|
200
|
+
'bzip2-1.0.6': 'Permissive',
|
|
201
|
+
'BSL-1.0': 'Permissive',
|
|
202
|
+
'PSF-2.0': 'Permissive'
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Normalize license identifier to canonical SPDX form
|
|
207
|
+
*
|
|
208
|
+
* Algorithm:
|
|
209
|
+
* 1. Handle UNKNOWN/empty cases
|
|
210
|
+
* 2. Clean whitespace
|
|
211
|
+
* 3. Exact match in normalization table
|
|
212
|
+
* 4. Case-insensitive match
|
|
213
|
+
* 5. Regex pattern matching (flexible)
|
|
214
|
+
* 6. Return as-is if already valid SPDX
|
|
215
|
+
*
|
|
216
|
+
* @param {string} license - License string from ecosystem metadata
|
|
217
|
+
* @returns {string} Canonical SPDX identifier or UNKNOWN
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* normalize('GPL-3.0') // → 'GPL-3.0-only'
|
|
221
|
+
* normalize('mit') // → 'MIT'
|
|
222
|
+
* normalize('Apache 2.0') // → 'Apache-2.0'
|
|
223
|
+
* normalize('') // → 'UNKNOWN'
|
|
224
|
+
*/
|
|
225
|
+
function normalize(license) {
|
|
226
|
+
if (!license || license === '' || /^none$/i.test(license) || license === 'UNKNOWN') {
|
|
227
|
+
return 'UNKNOWN'
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Clean: trim, normalize whitespace
|
|
231
|
+
const cleaned = license.trim().replace(/\s+/g, ' ')
|
|
232
|
+
|
|
233
|
+
// Exact match (fast path)
|
|
234
|
+
if (LICENSE_NORMALIZATIONS[cleaned]) {
|
|
235
|
+
return LICENSE_NORMALIZATIONS[cleaned]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Case-insensitive match
|
|
239
|
+
const lower = cleaned.toLowerCase()
|
|
240
|
+
for (const [key, value] of Object.entries(LICENSE_NORMALIZATIONS)) {
|
|
241
|
+
if (key.toLowerCase() === lower) {
|
|
242
|
+
return value
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ============================================
|
|
247
|
+
// Regex pattern matching (flexible normalization)
|
|
248
|
+
// ============================================
|
|
249
|
+
// CRITICAL: Order matters! Check more specific patterns first
|
|
250
|
+
// LGPL must be checked BEFORE GPL (LGPL contains "GPL" substring)
|
|
251
|
+
|
|
252
|
+
// Apache
|
|
253
|
+
if (/Apache.*2(\.0)?/i.test(cleaned)) return 'Apache-2.0'
|
|
254
|
+
if (/^ASL\s*2/i.test(cleaned)) return 'Apache-2.0'
|
|
255
|
+
|
|
256
|
+
// BSD (including full license text detection)
|
|
257
|
+
if (/BSD.*3.*Clause/i.test(cleaned)) return 'BSD-3-Clause'
|
|
258
|
+
if (/BSD.*2.*Clause/i.test(cleaned)) return 'BSD-2-Clause'
|
|
259
|
+
if (/^BSD$/i.test(cleaned)) return 'BSD-3-Clause'
|
|
260
|
+
// Detect full BSD license text (starts with "Redistribution and use")
|
|
261
|
+
if (/Redistribution and use in source and binary forms/i.test(cleaned)) {
|
|
262
|
+
return 'BSD-3-Clause' // Assume 3-Clause if full text
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// LGPL (CHECK BEFORE GPL - more specific pattern)
|
|
266
|
+
// CRITICAL: LGPL contains "GPL" substring, so must be checked first
|
|
267
|
+
if (/LGPL.*v?3/i.test(cleaned)) {
|
|
268
|
+
if (/\+|or.*later/i.test(cleaned)) return 'LGPL-3.0-or-later'
|
|
269
|
+
return 'LGPL-3.0-only'
|
|
270
|
+
}
|
|
271
|
+
if (/LGPL.*v?2/i.test(cleaned)) {
|
|
272
|
+
if (/\+|or.*later/i.test(cleaned)) return 'LGPL-2.1-or-later'
|
|
273
|
+
return 'LGPL-2.1-only'
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// AGPL (CHECK BEFORE GPL - AGPL contains "GPL" substring)
|
|
277
|
+
// CRITICAL: AGPL-3.0-only matches /GPL.*v?3/ if not checked first
|
|
278
|
+
if (/AGPL.*v?3/i.test(cleaned)) {
|
|
279
|
+
if (/\+|or.*later/i.test(cleaned)) return 'AGPL-3.0-or-later'
|
|
280
|
+
return 'AGPL-3.0-only'
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// GPL (CHECK AFTER LGPL AND AGPL - less specific pattern)
|
|
284
|
+
if (/GPL.*v?3/i.test(cleaned)) {
|
|
285
|
+
if (/\+|or.*later/i.test(cleaned)) return 'GPL-3.0-or-later'
|
|
286
|
+
return 'GPL-3.0-only'
|
|
287
|
+
}
|
|
288
|
+
if (/GPL.*v?2/i.test(cleaned)) {
|
|
289
|
+
if (/\+|or.*later/i.test(cleaned)) return 'GPL-2.0-or-later'
|
|
290
|
+
return 'GPL-2.0-only'
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// PSF
|
|
294
|
+
if (/Python Software Foundation/i.test(cleaned)) return 'PSF-2.0'
|
|
295
|
+
|
|
296
|
+
// ISC
|
|
297
|
+
if (/ISC.*License/i.test(cleaned)) return 'ISC'
|
|
298
|
+
|
|
299
|
+
// MPL
|
|
300
|
+
if (/Mozilla Public License.*2/i.test(cleaned)) return 'MPL-2.0'
|
|
301
|
+
|
|
302
|
+
// Proprietary
|
|
303
|
+
if (/proprietary/i.test(cleaned)) return 'LicenseRef-Proprietary'
|
|
304
|
+
if (/NVIDIA.*Proprietary/i.test(cleaned)) return 'LicenseRef-NVIDIA-Proprietary'
|
|
305
|
+
if (/commercial/i.test(cleaned)) return 'LicenseRef-Commercial'
|
|
306
|
+
|
|
307
|
+
// Return as-is if no match (might be valid SPDX already)
|
|
308
|
+
return cleaned
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get license family for compatibility upgrade path detection
|
|
313
|
+
*
|
|
314
|
+
* @param {string} license - Normalized SPDX identifier
|
|
315
|
+
* @returns {string} License family (GPL3, LGPL, MIT, BSD, Apache, etc.) or 'Unknown'
|
|
316
|
+
*
|
|
317
|
+
* @example
|
|
318
|
+
* getLicenseFamily('GPL-3.0-only') // → 'GPL3'
|
|
319
|
+
* getLicenseFamily('LGPL-2.1-or-later') // → 'LGPL-or-later'
|
|
320
|
+
* getLicenseFamily('MIT') // → 'MIT'
|
|
321
|
+
*/
|
|
322
|
+
function getLicenseFamily(license) {
|
|
323
|
+
if (!license || license === 'UNKNOWN') {
|
|
324
|
+
return 'Unknown'
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Normalize first to ensure consistent lookup
|
|
328
|
+
const normalized = normalize(license)
|
|
329
|
+
|
|
330
|
+
return LICENSE_FAMILIES[normalized] || 'Unknown'
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Check if two licenses are the same after normalization
|
|
335
|
+
* Handles SPDX deprecated identifiers (GPL-3.0 vs GPL-3.0-only)
|
|
336
|
+
*
|
|
337
|
+
* @param {string} license1 - First license identifier
|
|
338
|
+
* @param {string} license2 - Second license identifier
|
|
339
|
+
* @returns {boolean} True if same license after normalization
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* areSameLicense('GPL-3.0', 'GPL-3.0-only') // → true
|
|
343
|
+
* areSameLicense('mit', 'MIT') // → true
|
|
344
|
+
* areSameLicense('MIT', 'Apache-2.0') // → false
|
|
345
|
+
*/
|
|
346
|
+
function areSameLicense(license1, license2) {
|
|
347
|
+
return normalize(license1) === normalize(license2)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
module.exports = {
|
|
351
|
+
normalize,
|
|
352
|
+
getLicenseFamily,
|
|
353
|
+
areSameLicense,
|
|
354
|
+
// Export for testing
|
|
355
|
+
LICENSE_NORMALIZATIONS,
|
|
356
|
+
LICENSE_FAMILIES
|
|
357
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C/C++ Ecosystem Plugin (Conan)
|
|
3
|
+
* Scans Conan dependencies for license information
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs')
|
|
7
|
+
const path = require('path')
|
|
8
|
+
const { execSync } = require('child_process')
|
|
9
|
+
const { checkCompatibility } = require('../compat-checker')
|
|
10
|
+
const { showProgress } = require('../progress')
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Detect if this is a C/C++ Conan project
|
|
14
|
+
* @returns {boolean} True if conanfile.txt or conanfile.py exists
|
|
15
|
+
*/
|
|
16
|
+
function detect() {
|
|
17
|
+
return fs.existsSync('conanfile.txt') || fs.existsSync('conanfile.py')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse conanfile.txt to get dependency list
|
|
22
|
+
* @returns {string[]} Array of dependency references (e.g., ['boost/1.81.0', 'openssl/3.0.7'])
|
|
23
|
+
*/
|
|
24
|
+
function parseConanfile() {
|
|
25
|
+
let content = ''
|
|
26
|
+
|
|
27
|
+
if (fs.existsSync('conanfile.txt')) {
|
|
28
|
+
content = fs.readFileSync('conanfile.txt', 'utf8')
|
|
29
|
+
} else if (fs.existsSync('conanfile.py')) {
|
|
30
|
+
// For conanfile.py, we rely on conan info command instead
|
|
31
|
+
// Return empty array - getConanPackages will get the actual deps
|
|
32
|
+
return []
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const deps = []
|
|
36
|
+
const lines = content.split('\n')
|
|
37
|
+
let inRequires = false
|
|
38
|
+
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
const trimmed = line.trim()
|
|
41
|
+
|
|
42
|
+
// Check for [requires] section
|
|
43
|
+
if (trimmed === '[requires]') {
|
|
44
|
+
inRequires = true
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check for other sections (end of requires)
|
|
49
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
50
|
+
inRequires = false
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Parse dependency in requires section
|
|
55
|
+
if (inRequires && trimmed && !trimmed.startsWith('#')) {
|
|
56
|
+
deps.push(trimmed)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return deps
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get installed Conan packages via CLI
|
|
65
|
+
* Supports both Conan 1.x and Conan 2.x
|
|
66
|
+
* @returns {Array} Array of package objects with name, version, license, path
|
|
67
|
+
*/
|
|
68
|
+
function getConanPackages() {
|
|
69
|
+
let output
|
|
70
|
+
let isConan2 = false
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Try Conan 2.x command first
|
|
74
|
+
output = execSync('conan graph info . --format=json', {
|
|
75
|
+
encoding: 'utf8',
|
|
76
|
+
timeout: 30000, // 30 second timeout for graph operations
|
|
77
|
+
cwd: process.cwd(),
|
|
78
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
79
|
+
})
|
|
80
|
+
isConan2 = true
|
|
81
|
+
} catch (error) {
|
|
82
|
+
// Check if it's a "command not found" for conan itself
|
|
83
|
+
if (error.message.includes('command not found') ||
|
|
84
|
+
error.message.includes('not recognized') ||
|
|
85
|
+
error.message.includes('ENOENT')) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
'Conan not installed.\n\n' +
|
|
88
|
+
'Install Conan: pip install conan\n' +
|
|
89
|
+
'Or skip scanning: licenseguard init --noscan'
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// If Conan 2.x command failed, try Conan 1.x
|
|
94
|
+
if (error.message.includes('Unknown command') ||
|
|
95
|
+
error.message.includes('not a Conan command')) {
|
|
96
|
+
try {
|
|
97
|
+
output = execSync('conan info . --only requires --json', {
|
|
98
|
+
encoding: 'utf8',
|
|
99
|
+
timeout: 10000,
|
|
100
|
+
cwd: process.cwd(),
|
|
101
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
102
|
+
})
|
|
103
|
+
isConan2 = false
|
|
104
|
+
} catch (error2) {
|
|
105
|
+
// Check for project not installed
|
|
106
|
+
if (error2.message.includes('Unable to find') ||
|
|
107
|
+
error2.message.includes('not found in local cache')) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
'Conan dependencies not installed.\n\n' +
|
|
110
|
+
'Run: conan install .\n' +
|
|
111
|
+
'Or skip scanning: licenseguard init --noscan'
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
throw new Error('Failed to get Conan package info: ' + error2.message)
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
// Check for project not installed (Conan 2.x errors)
|
|
118
|
+
if (error.message.includes('No such file') ||
|
|
119
|
+
error.message.includes('does not exist') ||
|
|
120
|
+
error.message.includes('ERROR: Package')) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
'Conan dependencies not installed.\n\n' +
|
|
123
|
+
'Run: conan install . --build=missing\n' +
|
|
124
|
+
'Or skip scanning: licenseguard init --noscan'
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
throw new Error('Failed to get Conan package info: ' + error.message)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const data = JSON.parse(output)
|
|
132
|
+
|
|
133
|
+
if (isConan2) {
|
|
134
|
+
// Conan 2.x format: { "graph": { "nodes": { "0": {...}, "1": {...} } } }
|
|
135
|
+
const nodes = data.graph?.nodes || {}
|
|
136
|
+
return Object.values(nodes)
|
|
137
|
+
.filter(node => node.ref && node.ref !== 'conanfile') // Skip root node
|
|
138
|
+
.map(node => {
|
|
139
|
+
// Parse reference like "boost/1.81.0"
|
|
140
|
+
const ref = node.ref || ''
|
|
141
|
+
const match = ref.match(/^([^/]+)\/([^@#]+)/)
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
name: match ? match[1] : ref,
|
|
145
|
+
version: match ? match[2] : 'unknown',
|
|
146
|
+
license: node.license || null,
|
|
147
|
+
path: node.package_folder || node.source_folder || 'conan-cache'
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
} else {
|
|
151
|
+
// Conan 1.x format: array of package objects
|
|
152
|
+
return data.map(pkg => {
|
|
153
|
+
const ref = pkg.reference || pkg
|
|
154
|
+
const match = ref.match(/^([^/]+)\/([^@]+)/)
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
name: match ? match[1] : ref,
|
|
158
|
+
version: match ? match[2] : 'unknown',
|
|
159
|
+
license: pkg.license || null,
|
|
160
|
+
path: pkg.export_folder || pkg.package_folder || 'conan-cache'
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Extract license from Conan package metadata
|
|
168
|
+
* @param {Object} pkg - Package object from getConanPackages
|
|
169
|
+
* @returns {string} License identifier or 'UNKNOWN'
|
|
170
|
+
*/
|
|
171
|
+
function extractConanLicense(pkg) {
|
|
172
|
+
// If license is already in package info, use it
|
|
173
|
+
if (pkg.license) {
|
|
174
|
+
// Conan license can be a string or array
|
|
175
|
+
if (Array.isArray(pkg.license)) {
|
|
176
|
+
return pkg.license[0] || 'UNKNOWN'
|
|
177
|
+
}
|
|
178
|
+
return pkg.license
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Try to read license from conanfile.py in cache
|
|
182
|
+
if (pkg.path && pkg.path !== 'conan-cache') {
|
|
183
|
+
const conanfilePath = path.join(pkg.path, 'conanfile.py')
|
|
184
|
+
if (fs.existsSync(conanfilePath)) {
|
|
185
|
+
try {
|
|
186
|
+
const content = fs.readFileSync(conanfilePath, 'utf8')
|
|
187
|
+
// Look for license = "..." or license = '...'
|
|
188
|
+
const match = content.match(/license\s*=\s*["']([^"']+)["']/)
|
|
189
|
+
if (match) {
|
|
190
|
+
return match[1]
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
// Ignore read errors
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Try LICENSE file
|
|
198
|
+
const licensePath = path.join(pkg.path, 'licenses', 'LICENSE')
|
|
199
|
+
if (fs.existsSync(licensePath)) {
|
|
200
|
+
// Could parse LICENSE file content, but for now mark as found
|
|
201
|
+
return 'UNKNOWN' // Would need license detection logic
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return 'UNKNOWN'
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Scan all Conan dependencies for license compatibility
|
|
210
|
+
* @param {string} projectLicense - The project's license (SPDX format)
|
|
211
|
+
* @returns {Promise<Object>} Scan results
|
|
212
|
+
*/
|
|
213
|
+
async function scanDependencies(projectLicense) {
|
|
214
|
+
// Get installed packages via Conan CLI
|
|
215
|
+
const packages = getConanPackages()
|
|
216
|
+
|
|
217
|
+
const results = {
|
|
218
|
+
timestamp: new Date().toISOString(),
|
|
219
|
+
totalDependencies: packages.length,
|
|
220
|
+
compatible: 0,
|
|
221
|
+
incompatible: 0,
|
|
222
|
+
unknown: 0,
|
|
223
|
+
issues: []
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Scan each dependency
|
|
227
|
+
for (let i = 0; i < packages.length; i++) {
|
|
228
|
+
showProgress(i + 1, packages.length)
|
|
229
|
+
|
|
230
|
+
const pkg = packages[i]
|
|
231
|
+
const license = extractConanLicense(pkg)
|
|
232
|
+
|
|
233
|
+
// Check compatibility using universal compat-checker
|
|
234
|
+
const compatResult = checkCompatibility(projectLicense, license)
|
|
235
|
+
|
|
236
|
+
if (!compatResult.compatible) {
|
|
237
|
+
const isUnknown = license === 'UNKNOWN'
|
|
238
|
+
|
|
239
|
+
if (isUnknown) {
|
|
240
|
+
results.unknown++
|
|
241
|
+
} else {
|
|
242
|
+
results.incompatible++
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
results.issues.push({
|
|
246
|
+
package: `${pkg.name}@${pkg.version}`,
|
|
247
|
+
license: license,
|
|
248
|
+
type: isUnknown ? 'warning' : 'conflict',
|
|
249
|
+
reason: compatResult.reason,
|
|
250
|
+
location: pkg.path
|
|
251
|
+
})
|
|
252
|
+
} else {
|
|
253
|
+
results.compatible++
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return results
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = {
|
|
261
|
+
detect,
|
|
262
|
+
scanDependencies,
|
|
263
|
+
// Export internal functions for testing
|
|
264
|
+
parseConanfile,
|
|
265
|
+
getConanPackages,
|
|
266
|
+
extractConanLicense
|
|
267
|
+
}
|