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